activerecord-sqlanywhere-adapter-in4systems 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +33 -0
- data/LICENSE +23 -0
- data/README +56 -0
- data/lib/active_record/connection_adapters/sqlanywhere.rake +58 -0
- data/lib/active_record/connection_adapters/sqlanywhere_adapter.rb +793 -0
- data/lib/activerecord-sqlanywhere-adapter.rb +16 -0
- data/lib/arel/visitors/sqlanywhere.rb +125 -0
- data/test/connection.rb +25 -0
- metadata +88 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
=CHANGE LOG
|
2
|
+
|
3
|
+
=====0.2.0 -- 2010/12/02
|
4
|
+
- Added support for Rails 3.0.3
|
5
|
+
- Added support for Arel 2
|
6
|
+
- Removed test instructions for ActiveRecord 2.2.2
|
7
|
+
- Updated license to 2010
|
8
|
+
|
9
|
+
=====0.1.3 -- 2010/02/01
|
10
|
+
- Added :encoding option to connection string
|
11
|
+
- Fixed bug associated with dangling connections in development mode (http://groups.google.com/group/sql-anywhere-web-development/browse_thread/thread/79fa81bdfcf84c13/e29074e5b8b7ad6a?lnk=gst&q=activerecord#e29074e5b8b7ad6a)
|
12
|
+
|
13
|
+
=====0.1.2 -- 2008/12/30
|
14
|
+
- Fixed bug in ActiveRecord::ConnectionAdapters::SQLAnywhereAdapter#table_structure SQL (Paul Smith)
|
15
|
+
- Added options for :commlinks and :connection_name to database.yml configuration (Paul Smith)
|
16
|
+
- Fixed ActiveRecord::ConnectionAdapters::SQLAnywhereColumn.string_to_binary and binary_to_string (Paul Smith)
|
17
|
+
- Added :time as a native datatype (Paul Smith)
|
18
|
+
- Override SQLAnywhereAdapter#active? to prevent stale connections (Paul Smith)
|
19
|
+
- 'Fixed' coding style to match Rails standards (Paul Smith)
|
20
|
+
- Added temporary option for timestamp_format
|
21
|
+
- Fixed bug to let migrations drop columns with indexes
|
22
|
+
- Formatted code
|
23
|
+
- Fixed bug to raise proper exceptions when a query with a bad column in executed
|
24
|
+
|
25
|
+
=====0.1.1 -- 2008/11/06
|
26
|
+
- Changed file permissions on archives
|
27
|
+
- Changed archives to be specific to platform (.zip on windows, .tar.gz
|
28
|
+
otherwise)
|
29
|
+
- Removed the default rake task
|
30
|
+
|
31
|
+
=====0.1.0 -- 2008/10/15
|
32
|
+
- Initial Release
|
33
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
/*====================================================
|
2
|
+
*
|
3
|
+
* Copyright 2008-2010 iAnywhere Solutions, Inc.
|
4
|
+
*
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
* you may not use this file except in compliance with the License.
|
7
|
+
* You may obtain a copy of the License at
|
8
|
+
*
|
9
|
+
*
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
*
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
*
|
16
|
+
* See the License for the specific language governing permissions and
|
17
|
+
* limitations under the License.
|
18
|
+
*
|
19
|
+
* While not a requirement of the license, if you do modify this file, we
|
20
|
+
* would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
|
21
|
+
*
|
22
|
+
*
|
23
|
+
*====================================================*/
|
data/README
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
=SQL Anywhere ActiveRecord Driver
|
2
|
+
|
3
|
+
This is a SQL Anywhere driver for Ruby ActiveRecord. This driver requires the
|
4
|
+
native SQL Anywhere Ruby driver. To get the native driver, use:
|
5
|
+
|
6
|
+
gem install sqlanywhere
|
7
|
+
|
8
|
+
This driver is designed for use with ActiveRecord 3.0.3 and greater.
|
9
|
+
|
10
|
+
This driver is licensed under the Apache License, Version 2.
|
11
|
+
|
12
|
+
==Making a Connection
|
13
|
+
|
14
|
+
The following code is a sample database configuration object.
|
15
|
+
|
16
|
+
ActiveRecord::Base.configurations = {
|
17
|
+
'arunit' => {
|
18
|
+
:adapter => 'sqlanywhere',
|
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
|
+
:encoding => 'Windows-1252', #equivalent to the "CharSet" parameter
|
24
|
+
:commlinks => 'TCPIP()', #equivalent to the "Commlinks" parameter
|
25
|
+
:connection_name => 'Rails' #equivalent to the "ConnectionName" parameter
|
26
|
+
}
|
27
|
+
|
28
|
+
==Running the ActiveRecord Unit Test Suite
|
29
|
+
|
30
|
+
1. Open <tt><ACTIVERECORD_INSTALL_DIR>/rakefile</tt> and modify the line:
|
31
|
+
|
32
|
+
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
|
33
|
+
|
34
|
+
to include <tt>sqlanywhere</tt>. It should now look like:
|
35
|
+
|
36
|
+
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase sqlanywhere )
|
37
|
+
|
38
|
+
2. Create directory to hold the connection definition:
|
39
|
+
|
40
|
+
mkdir <ACTIVERECORD_INSTALL_DIR>/test/connections/native_sqlanywhere
|
41
|
+
|
42
|
+
3. Copy <tt>test/connection.rb</tt> into the newly created directory.
|
43
|
+
|
44
|
+
4. Create the two test databases. These can be created in any directory.
|
45
|
+
|
46
|
+
dbinit -c arunit
|
47
|
+
dbinit -c arunit2
|
48
|
+
dbsrv11 arunit arunit2
|
49
|
+
|
50
|
+
<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].
|
51
|
+
|
52
|
+
6. Run the unit test suite from the ActiveRecord install directory:
|
53
|
+
|
54
|
+
rake test_sqlanywhere
|
55
|
+
|
56
|
+
<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].
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Taken from https://github.com/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced.rake
|
2
|
+
|
3
|
+
# implementation idea taken from JDBC adapter
|
4
|
+
# added possibility to execute previously defined task (passed as argument to task block)
|
5
|
+
def redefine_task(*args, &block)
|
6
|
+
task_name = Hash === args.first ? args.first.keys[0] : args.first
|
7
|
+
existing_task = Rake.application.lookup task_name
|
8
|
+
existing_actions = nil
|
9
|
+
if existing_task
|
10
|
+
class << existing_task; public :instance_variable_set, :instance_variable_get; end
|
11
|
+
existing_task.instance_variable_set "@prerequisites", FileList[]
|
12
|
+
existing_actions = existing_task.instance_variable_get "@actions"
|
13
|
+
existing_task.instance_variable_set "@actions", []
|
14
|
+
end
|
15
|
+
task(*args) do
|
16
|
+
block.call(existing_actions)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# https://github.com/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced.rake
|
22
|
+
|
23
|
+
if defined?(drop_database) == 'method'
|
24
|
+
def drop_database_with_sqlanywhere(config)
|
25
|
+
if config['adapter'] == 'sqlanywhere'
|
26
|
+
ActiveRecord::Base.establish_connection(config)
|
27
|
+
ActiveRecord::Base.connection.purge_database
|
28
|
+
else
|
29
|
+
drop_database_without_sqlanywhere(config)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
alias :drop_database_without_sqlanywhere :drop_database
|
33
|
+
alias :drop_database :drop_database_with_sqlanywhere
|
34
|
+
end
|
35
|
+
|
36
|
+
namespace :db do
|
37
|
+
namespace :test do
|
38
|
+
redefine_task :purge => :environment do |existing_actions|
|
39
|
+
abcs = ActiveRecord::Base.configurations
|
40
|
+
if abcs['test']['adapter'] == 'sqlanywhere'
|
41
|
+
ActiveRecord::Base.establish_connection(:test)
|
42
|
+
ActiveRecord::Base.connection.purge_database
|
43
|
+
else
|
44
|
+
Array(existing_actions).each{|action| action.call}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
namespace :schema do
|
50
|
+
redefine_task :dump => :environment do |existing_actions|
|
51
|
+
if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'sqlanywhere'
|
52
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
|
53
|
+
ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::Base.connection.viewed_tables
|
54
|
+
end
|
55
|
+
Array(existing_actions).each{|action| action.call}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,793 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
#====================================================
|
3
|
+
#
|
4
|
+
# Copyright 2008-2010 iAnywhere Solutions, Inc.
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
#
|
17
|
+
# See the License for the specific language governing permissions and
|
18
|
+
# limitations under the License.
|
19
|
+
#
|
20
|
+
# While not a requirement of the license, if you do modify this file, we
|
21
|
+
# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
|
22
|
+
#
|
23
|
+
#
|
24
|
+
#====================================================
|
25
|
+
|
26
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
27
|
+
require 'arel/visitors/sqlanywhere.rb'
|
28
|
+
|
29
|
+
# Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
|
30
|
+
class SA
|
31
|
+
include Singleton
|
32
|
+
attr_accessor :api
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
require 'sqlanywhere' unless defined? SQLAnywhere
|
36
|
+
@api = SQLAnywhere::SQLAnywhereInterface.new()
|
37
|
+
raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0
|
38
|
+
raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ActiveRecord
|
43
|
+
class Base
|
44
|
+
DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' }
|
45
|
+
# Main connection function to SQL Anywhere
|
46
|
+
# Connection Adapter takes four parameters:
|
47
|
+
# * :database (required, no default). Corresponds to "DatabaseName=" in connection string
|
48
|
+
# * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string
|
49
|
+
# * :username (optional, default to 'dba')
|
50
|
+
# * :password (optional, deafult to 'sql')
|
51
|
+
# * :encoding (optional, defaults to charset of OS)
|
52
|
+
# * :commlinks (optional). Corresponds to "CommLinks=" in connection string
|
53
|
+
# * :connection_name (optional). Corresponds to "ConnectionName=" in connection string
|
54
|
+
|
55
|
+
def self.sqlanywhere_connection(config)
|
56
|
+
|
57
|
+
if config[:connection_string]
|
58
|
+
connection_string = config[:connection_string]
|
59
|
+
else
|
60
|
+
config = DEFAULT_CONFIG.merge(config)
|
61
|
+
|
62
|
+
raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database)
|
63
|
+
|
64
|
+
connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};"
|
65
|
+
connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil?
|
66
|
+
connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil?
|
67
|
+
connection_string += "CharSet=#{config[:encoding]};" unless config[:encoding].nil?
|
68
|
+
connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default)
|
69
|
+
end
|
70
|
+
|
71
|
+
db = SA.instance.api.sqlany_new_connection()
|
72
|
+
|
73
|
+
ConnectionAdapters::SQLAnywhereAdapter.new(db, logger, connection_string)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module ConnectionAdapters
|
78
|
+
class SQLAnywhereException < StandardError
|
79
|
+
attr_reader :errno
|
80
|
+
attr_reader :sql
|
81
|
+
|
82
|
+
def initialize(message, errno, sql)
|
83
|
+
super(message)
|
84
|
+
@errno = errno
|
85
|
+
@sql = sql
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class SQLAnywhereColumn < Column
|
90
|
+
private
|
91
|
+
# Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types
|
92
|
+
def simplified_type(field_type)
|
93
|
+
return :boolean if field_type =~ /tinyint/i
|
94
|
+
return :boolean if field_type =~ /bit/i
|
95
|
+
return :text if field_type =~ /long varchar/i
|
96
|
+
return :string if field_type =~ /varchar/i
|
97
|
+
return :binary if field_type =~ /long binary/i
|
98
|
+
return :datetime if field_type =~ /timestamp/i
|
99
|
+
return :integer if field_type =~ /smallint|bigint/i
|
100
|
+
return :text if field_type =~ /xml/i
|
101
|
+
return :integer if field_type =~ /uniqueidentifier/i
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def extract_limit(sql_type)
|
106
|
+
case sql_type
|
107
|
+
when /^tinyint/i
|
108
|
+
1
|
109
|
+
when /^smallint/i
|
110
|
+
2
|
111
|
+
when /^integer/i
|
112
|
+
4
|
113
|
+
when /^bigint/i
|
114
|
+
8
|
115
|
+
else super
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
# Handles the encoding of a binary object into SQL Anywhere
|
121
|
+
# SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number
|
122
|
+
# This function encodes the binary string in this format
|
123
|
+
def self.string_to_binary(value)
|
124
|
+
"\\x" + value.unpack("H*")[0].scan(/../).join("\\x")
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.binary_to_string(value)
|
128
|
+
value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex }
|
129
|
+
end
|
130
|
+
|
131
|
+
# Should override the time column values.
|
132
|
+
# Sybase doesn't like the time zones.
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
class SQLAnywhereAdapter < AbstractAdapter
|
137
|
+
def initialize( connection, logger, connection_string = "") #:nodoc:
|
138
|
+
super(connection, logger)
|
139
|
+
@auto_commit = true
|
140
|
+
@affected_rows = 0
|
141
|
+
@connection_string = connection_string
|
142
|
+
@visitor = Arel::Visitors::SQLAnywhere.new self
|
143
|
+
connect!
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.visitor_for(pool)
|
147
|
+
config = pool.spec.config
|
148
|
+
|
149
|
+
if config.fetch(:prepared_statements) {true}
|
150
|
+
Arel::Visitors::SQLAnywhere.new pool
|
151
|
+
else
|
152
|
+
BindSubstitution.new pool
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def adapter_name #:nodoc:
|
157
|
+
'SQLAnywhere'
|
158
|
+
end
|
159
|
+
|
160
|
+
def supports_migrations? #:nodoc:
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
164
|
+
def requires_reloading?
|
165
|
+
true
|
166
|
+
end
|
167
|
+
|
168
|
+
def active?
|
169
|
+
# The liveness variable is used a low-cost "no-op" to test liveness
|
170
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1
|
171
|
+
rescue
|
172
|
+
false
|
173
|
+
end
|
174
|
+
|
175
|
+
def disconnect!
|
176
|
+
result = SA.instance.api.sqlany_disconnect( @connection )
|
177
|
+
super
|
178
|
+
end
|
179
|
+
|
180
|
+
def reconnect!
|
181
|
+
disconnect!
|
182
|
+
connect!
|
183
|
+
end
|
184
|
+
|
185
|
+
def supports_count_distinct? #:nodoc:
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
def supports_autoincrement? #:nodoc:
|
190
|
+
true
|
191
|
+
end
|
192
|
+
|
193
|
+
# Maps native ActiveRecord/Ruby types into SQLAnywhere types
|
194
|
+
# TINYINTs are treated as the default boolean value
|
195
|
+
# ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not
|
196
|
+
# As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and
|
197
|
+
# should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers
|
198
|
+
def native_database_types #:nodoc:
|
199
|
+
{
|
200
|
+
:primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL',
|
201
|
+
:string => { :name => "varchar", :limit => 255 },
|
202
|
+
:text => { :name => "long varchar" },
|
203
|
+
:integer => { :name => "integer", :limit => 4 },
|
204
|
+
:float => { :name => "float" },
|
205
|
+
:decimal => { :name => "decimal" },
|
206
|
+
:datetime => { :name => "datetime" },
|
207
|
+
:timestamp => { :name => "datetime" },
|
208
|
+
:time => { :name => "time" },
|
209
|
+
:date => { :name => "date" },
|
210
|
+
:binary => { :name => "binary" },
|
211
|
+
:boolean => { :name => "tinyint", :limit => 1}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
# QUOTING ==================================================
|
216
|
+
|
217
|
+
# Applies quotations around column names in generated queries
|
218
|
+
def quote_column_name(name) #:nodoc:
|
219
|
+
%Q("#{name}")
|
220
|
+
end
|
221
|
+
|
222
|
+
# Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord.
|
223
|
+
# ActiveRecord requires that any strings it inserts into databases must escape the backslash (\).
|
224
|
+
# Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped.
|
225
|
+
def quote(value, column = nil)
|
226
|
+
case value
|
227
|
+
when String, ActiveSupport::Multibyte::Chars
|
228
|
+
value_S = value.to_s
|
229
|
+
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
230
|
+
"'#{column.class.string_to_binary(value_S)}'"
|
231
|
+
else
|
232
|
+
super(value, column)
|
233
|
+
end
|
234
|
+
else
|
235
|
+
super(value, column)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def quoted_true
|
240
|
+
'1'
|
241
|
+
end
|
242
|
+
|
243
|
+
def quoted_false
|
244
|
+
'0'
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# This function (distinct) is based on the Oracle Enhacned ActiveRecord driver maintained by Raimonds Simanovskis (2010)
|
249
|
+
# (https://github.com/rsim/oracle-enhanced)
|
250
|
+
def distinct(columns, order_by) #:nodoc:
|
251
|
+
return "DISTINCT #{columns}" if order_by.blank?
|
252
|
+
|
253
|
+
# construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
|
254
|
+
# FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
|
255
|
+
order_columns = if order_by.is_a?(String)
|
256
|
+
order_by.split(',').map { |s| s.strip }.reject(&:blank?)
|
257
|
+
else # in latest ActiveRecord versions order_by is already Array
|
258
|
+
order_by
|
259
|
+
end
|
260
|
+
order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
|
261
|
+
# remove any ASC/DESC modifiers
|
262
|
+
value = c =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : c
|
263
|
+
"FIRST_VALUE(#{value}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
|
264
|
+
end
|
265
|
+
sql = "DISTINCT #{columns}, "
|
266
|
+
sql << order_columns * ", "
|
267
|
+
end
|
268
|
+
|
269
|
+
# The database execution function
|
270
|
+
def execute(sql, name = nil) #:nodoc:
|
271
|
+
if name == :skip_logging
|
272
|
+
r = SA.instance.api.sqlany_execute_immediate(@connection, sql)
|
273
|
+
sqlanywhere_error_test(sql) if r==0
|
274
|
+
else
|
275
|
+
log(sql, name) { execute(sql, :skip_logging) }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def sqlanywhere_error_test(sql = '')
|
280
|
+
error_code, error_message = SA.instance.api.sqlany_error(@connection)
|
281
|
+
if error_code != 0
|
282
|
+
sqlanywhere_error(error_code, error_message, sql)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def sqlanywhere_error(code, message, sql)
|
287
|
+
raise SQLAnywhereException.new(message, code, sql)
|
288
|
+
end
|
289
|
+
|
290
|
+
def translate_exception(exception, message)
|
291
|
+
return super unless exception.respond_to?(:errno)
|
292
|
+
case exception.errno
|
293
|
+
when -143
|
294
|
+
if exception.sql !~ /^SELECT/i then
|
295
|
+
raise ActiveRecord::ActiveRecordError.new(message)
|
296
|
+
else
|
297
|
+
super
|
298
|
+
end
|
299
|
+
when -194
|
300
|
+
raise InvalidForeignKey.new(message, exception)
|
301
|
+
when -196
|
302
|
+
raise RecordNotUnique.new(message, exception)
|
303
|
+
when -183
|
304
|
+
raise ArgumentError, message
|
305
|
+
else
|
306
|
+
super
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# The database update function.
|
311
|
+
def update_sql(sql, name = nil)
|
312
|
+
execute( sql, name )
|
313
|
+
return @affected_rows
|
314
|
+
end
|
315
|
+
|
316
|
+
# The database delete function.
|
317
|
+
def delete_sql(sql, name = nil) #:nodoc:
|
318
|
+
execute( sql, name )
|
319
|
+
return @affected_rows
|
320
|
+
end
|
321
|
+
|
322
|
+
# The database insert function.
|
323
|
+
# ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
|
324
|
+
# by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
|
325
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
326
|
+
execute(sql, name)
|
327
|
+
|
328
|
+
retval = last_inserted_id(nil)
|
329
|
+
retval = id_value if retval == 0
|
330
|
+
return retval
|
331
|
+
end
|
332
|
+
|
333
|
+
def exec_delete(sql, name = 'SQL', binds = [])
|
334
|
+
exec_query(sql, name, binds)
|
335
|
+
@affected_rows
|
336
|
+
end
|
337
|
+
alias :exec_update :exec_delete
|
338
|
+
|
339
|
+
def last_inserted_id(result)
|
340
|
+
identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity')
|
341
|
+
raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil?
|
342
|
+
SA.instance.api.sqlany_fetch_next(identity)
|
343
|
+
retval = SA.instance.api.sqlany_get_column(identity, 0)[1]
|
344
|
+
SA.instance.api.sqlany_free_stmt(identity)
|
345
|
+
|
346
|
+
return retval
|
347
|
+
end
|
348
|
+
|
349
|
+
# Returns a query as an array of arrays
|
350
|
+
def select_rows(sql, name = nil)
|
351
|
+
rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
|
352
|
+
raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
|
353
|
+
record = []
|
354
|
+
while SA.instance.api.sqlany_fetch_next(rs) == 1
|
355
|
+
max_cols = SA.instance.api.sqlany_num_cols(rs)
|
356
|
+
result = Array.new(max_cols)
|
357
|
+
max_cols.times do |cols|
|
358
|
+
result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1]
|
359
|
+
end
|
360
|
+
record << result
|
361
|
+
end
|
362
|
+
SA.instance.api.sqlany_free_stmt(rs)
|
363
|
+
return record
|
364
|
+
end
|
365
|
+
|
366
|
+
def begin_db_transaction #:nodoc:
|
367
|
+
@auto_commit = false;
|
368
|
+
end
|
369
|
+
|
370
|
+
def commit_db_transaction #:nodoc:
|
371
|
+
SA.instance.api.sqlany_commit(@connection)
|
372
|
+
@auto_commit = true;
|
373
|
+
end
|
374
|
+
|
375
|
+
def rollback_db_transaction #:nodoc:
|
376
|
+
SA.instance.api.sqlany_rollback(@connection)
|
377
|
+
@auto_commit = true;
|
378
|
+
end
|
379
|
+
|
380
|
+
def add_lock!(sql, options) #:nodoc:
|
381
|
+
sql
|
382
|
+
end
|
383
|
+
|
384
|
+
# SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes
|
385
|
+
# must be captured when generating the SQL and replaced with the appropriate size.
|
386
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
387
|
+
type = type.to_sym
|
388
|
+
if native = native_database_types[type]
|
389
|
+
if type == :integer
|
390
|
+
case limit
|
391
|
+
when 1
|
392
|
+
column_type_sql = 'tinyint'
|
393
|
+
when 2
|
394
|
+
column_type_sql = 'smallint'
|
395
|
+
when 3..4
|
396
|
+
column_type_sql = 'integer'
|
397
|
+
when 5..8
|
398
|
+
column_type_sql = 'bigint'
|
399
|
+
else
|
400
|
+
column_type_sql = 'integer'
|
401
|
+
end
|
402
|
+
column_type_sql
|
403
|
+
elsif type == :string and !limit.nil?
|
404
|
+
"varchar (#{limit})"
|
405
|
+
elsif type == :boolean
|
406
|
+
column_type_sql = 'tinyint'
|
407
|
+
else
|
408
|
+
super(type, limit, precision, scale)
|
409
|
+
end
|
410
|
+
else
|
411
|
+
super(type, limit, precision, scale)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def viewed_tables(name = nil)
|
416
|
+
list_of_tables(['view'], name)
|
417
|
+
end
|
418
|
+
|
419
|
+
def base_tables(name = nil)
|
420
|
+
list_of_tables(['base'], name)
|
421
|
+
end
|
422
|
+
|
423
|
+
# Do not return SYS-owned or DBO-owned tables or RS_systabgroup-owned
|
424
|
+
def tables(name = nil) #:nodoc:
|
425
|
+
list_of_tables(['base', 'view'])
|
426
|
+
end
|
427
|
+
|
428
|
+
def columns(table_name, name = nil) #:nodoc:
|
429
|
+
table_structure(table_name).map do |field|
|
430
|
+
SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1))
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def indexes(table_name, name = nil) #:nodoc:
|
435
|
+
if @major_version <= 11 # the sql doesn't work in older databases.
|
436
|
+
return []
|
437
|
+
end
|
438
|
+
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"
|
439
|
+
select(sql, name).map do |row|
|
440
|
+
index = IndexDefinition.new(table_name, row['index_name'])
|
441
|
+
index.unique = row['unique'] == 1
|
442
|
+
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']}'"
|
443
|
+
index.columns = select(sql).map { |col| col['column_name'] }
|
444
|
+
index
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def primary_key(table_name) #:nodoc:
|
449
|
+
sql = "SELECT cname from SYS.SYSCOLUMNS where tname = '#{table_name}' and in_primary_key = 'Y'"
|
450
|
+
rs = exec_query(sql)
|
451
|
+
if !rs.nil? and !rs.first.nil?
|
452
|
+
rs.first['cname']
|
453
|
+
else
|
454
|
+
nil
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
def remove_index(table_name, options={}) #:nodoc:
|
459
|
+
execute "DROP INDEX #{quote_table_name(table_name)}.#{quote_column_name(index_name(table_name, options))}"
|
460
|
+
end
|
461
|
+
|
462
|
+
def rename_table(name, new_name)
|
463
|
+
execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}"
|
464
|
+
end
|
465
|
+
|
466
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
467
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
|
468
|
+
end
|
469
|
+
|
470
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
471
|
+
unless null || default.nil?
|
472
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
473
|
+
end
|
474
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL")
|
475
|
+
end
|
476
|
+
|
477
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
478
|
+
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])}"
|
479
|
+
add_column_options!(add_column_sql, options)
|
480
|
+
add_column_sql << ' NULL' if options[:null]
|
481
|
+
execute(add_column_sql)
|
482
|
+
end
|
483
|
+
|
484
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
485
|
+
if column_name.downcase == new_column_name.downcase
|
486
|
+
whine = "if_the_only_change_is_case_sqlanywhere_doesnt_rename_the_column"
|
487
|
+
rename_column table_name, column_name, "#{new_column_name}#{whine}"
|
488
|
+
rename_column table_name, "#{new_column_name}#{whine}", new_column_name
|
489
|
+
else
|
490
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def remove_column(table_name, *column_names)
|
495
|
+
column_names = column_names.flatten
|
496
|
+
column_names.zip(columns_for_remove(table_name, *column_names)).each do |unquoted_column_name, column_name|
|
497
|
+
sql = <<-SQL
|
498
|
+
SELECT "index_name" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX
|
499
|
+
WHERE "column_name" = '#{unquoted_column_name}' AND "table_name" = '#{table_name}'
|
500
|
+
SQL
|
501
|
+
select(sql, nil).each do |row|
|
502
|
+
execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\""
|
503
|
+
end
|
504
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
def purge_database
|
510
|
+
base_tables.each do |base_table_name|
|
511
|
+
drop_table(base_table_name)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
def select(sql, name = nil, binds = []) #:nodoc:
|
516
|
+
exec_query(sql, name, binds).to_a
|
517
|
+
end
|
518
|
+
|
519
|
+
protected
|
520
|
+
|
521
|
+
def list_of_tables(types, name = nil)
|
522
|
+
sql = "SELECT table_name FROM SYS.SYSTABLE WHERE table_type in (#{types.map{|t| quote(t)}.join(', ')}) and creator NOT IN (0,3,5)"
|
523
|
+
select(sql, name).map { |row| row["table_name"] }
|
524
|
+
end
|
525
|
+
|
526
|
+
# ActiveRecord uses the OFFSET/LIMIT keywords at the end of query to limit the number of items in the result set.
|
527
|
+
# This syntax is NOT supported by SQL Anywhere. In previous versions of this adapter this adapter simply
|
528
|
+
# overrode the add_limit_offset function and added the appropriate TOP/START AT keywords to the start of the query.
|
529
|
+
# However, this will not work for cases where add_limit_offset is being used in a subquery since add_limit_offset
|
530
|
+
# is called with the WHERE clause.
|
531
|
+
#
|
532
|
+
# As a result, the following function must be called before every SELECT statement against the database. It
|
533
|
+
# recursivly walks through all subqueries in the SQL statment and replaces the instances of OFFSET/LIMIT with the
|
534
|
+
# corresponding TOP/START AT. It was my intent to do the entire thing using regular expressions, but it would seem
|
535
|
+
# that it is not possible given that it must count levels of nested brackets.
|
536
|
+
def modify_limit_offset(sql)
|
537
|
+
modified_sql = ""
|
538
|
+
subquery_sql = ""
|
539
|
+
in_single_quote = false
|
540
|
+
in_double_quote = false
|
541
|
+
nesting_level = 0
|
542
|
+
if sql =~ /(OFFSET|LIMIT)/xmi then
|
543
|
+
if sql =~ /\(/ then
|
544
|
+
sql.split(//).each_with_index do |x, i|
|
545
|
+
case x[0]
|
546
|
+
when 40 # left brace - (
|
547
|
+
modified_sql << x if nesting_level == 0
|
548
|
+
subquery_sql << x if nesting_level > 0
|
549
|
+
nesting_level = nesting_level + 1 unless in_double_quote || in_single_quote
|
550
|
+
when 41 # right brace - )
|
551
|
+
nesting_level = nesting_level - 1 unless in_double_quote || in_single_quote
|
552
|
+
if nesting_level == 0 and !in_double_quote and !in_single_quote then
|
553
|
+
modified_sql << modify_limit_offset(subquery_sql)
|
554
|
+
subquery_sql = ""
|
555
|
+
end
|
556
|
+
modified_sql << x if nesting_level == 0
|
557
|
+
subquery_sql << x if nesting_level > 0
|
558
|
+
when 39 # single quote - '
|
559
|
+
in_single_quote = in_single_quote ^ true unless in_double_quote
|
560
|
+
modified_sql << x if nesting_level == 0
|
561
|
+
subquery_sql << x if nesting_level > 0
|
562
|
+
when 34 # double quote - "
|
563
|
+
in_double_quote = in_double_quote ^ true unless in_single_quote
|
564
|
+
modified_sql << x if nesting_level == 0
|
565
|
+
subquery_sql << x if nesting_level > 0
|
566
|
+
else
|
567
|
+
modified_sql << x if nesting_level == 0
|
568
|
+
subquery_sql << x if nesting_level > 0
|
569
|
+
end
|
570
|
+
raise ActiveRecord::StatementInvalid.new("Braces do not match: #{sql}") if nesting_level < 0
|
571
|
+
end
|
572
|
+
else
|
573
|
+
modified_sql = sql
|
574
|
+
end
|
575
|
+
raise ActiveRecord::StatementInvalid.new("Quotes do not match: #{sql}") if in_double_quote or in_single_quote
|
576
|
+
return "" if modified_sql.nil?
|
577
|
+
select_components = modified_sql.scan(/\ASELECT\s+(DISTINCT)?(.*?)(?:\s+LIMIT\s+(.*?))?(?:\s+OFFSET\s+(.*?))?\Z/xmi)
|
578
|
+
return modified_sql if select_components[0].nil?
|
579
|
+
final_sql = "SELECT #{select_components[0][0]} "
|
580
|
+
final_sql << "TOP #{select_components[0][2].nil? ? 1000000 : select_components[0][2]} "
|
581
|
+
final_sql << "START AT #{(select_components[0][3].to_i + 1).to_s} " unless select_components[0][3].nil?
|
582
|
+
final_sql << "#{select_components[0][1]}"
|
583
|
+
return final_sql
|
584
|
+
else
|
585
|
+
return sql
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
# Queries the structure of a table including the columns names, defaults, type, and nullability
|
590
|
+
# ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
|
591
|
+
# chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
|
592
|
+
# numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
|
593
|
+
# Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
|
594
|
+
# Alos, ActiveRecord expects an autoincrement column to have default value of NULL
|
595
|
+
|
596
|
+
def table_structure(table_name)
|
597
|
+
sql = <<-SQL
|
598
|
+
SELECT SYS.SYSCOLUMN.column_name AS name,
|
599
|
+
if left("default",1)='''' then substring("default", 2, length("default")-2) // remove the surrounding quotes
|
600
|
+
else NULLIF(SYS.SYSCOLUMN."default", 'autoincrement')
|
601
|
+
endif AS "default",
|
602
|
+
IF SYS.SYSCOLUMN.domain_id IN (7,8,9,11,33,34,35,3,27) THEN
|
603
|
+
IF SYS.SYSCOLUMN.domain_id IN (3,27) THEN
|
604
|
+
SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ',' || SYS.SYSCOLUMN.scale || ')'
|
605
|
+
ELSE
|
606
|
+
SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ')'
|
607
|
+
ENDIF
|
608
|
+
ELSE
|
609
|
+
SYS.SYSDOMAIN.domain_name
|
610
|
+
ENDIF AS domain,
|
611
|
+
IF SYS.SYSCOLUMN.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls
|
612
|
+
FROM
|
613
|
+
SYS.SYSCOLUMN
|
614
|
+
INNER JOIN SYS.SYSTABLE ON SYS.SYSCOLUMN.table_id = SYS.SYSTABLE.table_id
|
615
|
+
INNER JOIN SYS.SYSDOMAIN ON SYS.SYSCOLUMN.domain_id = SYS.SYSDOMAIN.domain_id
|
616
|
+
WHERE
|
617
|
+
SYS.SYSTABLE.creator = 1 AND
|
618
|
+
table_name = '#{table_name}'
|
619
|
+
SQL
|
620
|
+
structure = exec_query(sql, :skip_logging)
|
621
|
+
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure == false
|
622
|
+
structure
|
623
|
+
end
|
624
|
+
|
625
|
+
# Required to prevent DEFAULT NULL being added to primary keys
|
626
|
+
def options_include_default?(options)
|
627
|
+
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
628
|
+
end
|
629
|
+
|
630
|
+
private
|
631
|
+
|
632
|
+
def connect!
|
633
|
+
result = SA.instance.api.sqlany_connect(@connection, @connection_string)
|
634
|
+
if result == 1 then
|
635
|
+
set_connection_options
|
636
|
+
else
|
637
|
+
error = SA.instance.api.sqlany_error(@connection)
|
638
|
+
raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
|
639
|
+
end
|
640
|
+
version = exec_query('select @@version').rows[0][0]
|
641
|
+
@major_version = /^\d+/.match(version).to_s.to_i
|
642
|
+
end
|
643
|
+
|
644
|
+
def set_connection_options
|
645
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil
|
646
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil
|
647
|
+
#SA.instance.api.sqlany_execute_immediate(@connection, "SET OPTION reserved_keywords = 'LIMIT'") rescue nil
|
648
|
+
# The liveness variable is used a low-cost "no-op" to test liveness
|
649
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil
|
650
|
+
end
|
651
|
+
|
652
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
653
|
+
log(sql, name, binds) do
|
654
|
+
stmt = SA.instance.api.sqlany_prepare(@connection, sql)
|
655
|
+
|
656
|
+
if stmt.nil?
|
657
|
+
sqlanywhere_error_test(sql)
|
658
|
+
end
|
659
|
+
|
660
|
+
for i in 0...binds.length
|
661
|
+
bind_type = binds[i][0].type
|
662
|
+
bind_value = binds[i][1]
|
663
|
+
result, bind_param = SA.instance.api.sqlany_describe_bind_param(stmt, i)
|
664
|
+
sqlanywhere_error_test(sql) if result==0
|
665
|
+
|
666
|
+
bind_param.set_direction(:input)
|
667
|
+
if bind_value.nil?
|
668
|
+
bind_param.set_value(nil)
|
669
|
+
elsif bind_type == :datetime
|
670
|
+
bind_param.set_value(bind_value.to_datetime.to_s :db)
|
671
|
+
elsif bind_type == :boolean
|
672
|
+
bind_param.set_value(bind_value ? 1 : 0)
|
673
|
+
elsif bind_type == :decimal
|
674
|
+
bind_param.set_value(bind_value.to_s)
|
675
|
+
elsif bind_type == :date
|
676
|
+
bind_param.set_value(bind_value.to_s)
|
677
|
+
else
|
678
|
+
bind_param.set_value(bind_value)
|
679
|
+
end
|
680
|
+
result = SA.instance.api.sqlany_bind_param(stmt, i, bind_param)
|
681
|
+
sqlanywhere_error_test(sql) if result==0
|
682
|
+
|
683
|
+
end
|
684
|
+
|
685
|
+
if SA.instance.api.sqlany_execute(stmt) == 0
|
686
|
+
sqlanywhere_error_test(sql)
|
687
|
+
end
|
688
|
+
|
689
|
+
fields = []
|
690
|
+
native_types = []
|
691
|
+
|
692
|
+
num_cols = SA.instance.api.sqlany_num_cols(stmt)
|
693
|
+
sqlanywhere_error_test(sql) if num_cols == -1
|
694
|
+
|
695
|
+
for i in 0...num_cols
|
696
|
+
result, col_num, name, ruby_type, native_type, precision, scale, max_size, nullable = SA.instance.api.sqlany_get_column_info(stmt, i)
|
697
|
+
sqlanywhere_error_test(sql) if result==0
|
698
|
+
fields << name
|
699
|
+
native_types << native_type
|
700
|
+
end
|
701
|
+
rows = []
|
702
|
+
while SA.instance.api.sqlany_fetch_next(stmt) == 1
|
703
|
+
row = []
|
704
|
+
for i in 0...num_cols
|
705
|
+
r, value = SA.instance.api.sqlany_get_column(stmt, i)
|
706
|
+
row << native_type_to_ruby_type(native_types[i], value)
|
707
|
+
end
|
708
|
+
rows << row
|
709
|
+
end
|
710
|
+
SA.instance.api.sqlany_free_stmt(stmt)
|
711
|
+
|
712
|
+
if @auto_commit
|
713
|
+
result = SA.instance.api.sqlany_commit(@connection)
|
714
|
+
sqlanywhere_error_test(sql) if result==0
|
715
|
+
end
|
716
|
+
return ActiveRecord::Result.new(fields, rows)
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
def query(sql)
|
721
|
+
return if sql.nil?
|
722
|
+
#sql = modify_limit_offset(sql)
|
723
|
+
|
724
|
+
# ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer.
|
725
|
+
return Array.new() if sql =~ /TOP 0/i
|
726
|
+
|
727
|
+
# Executes the query, iterates through the results, and builds an array of hashes.
|
728
|
+
rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
|
729
|
+
if rs.nil?
|
730
|
+
result, errstr = SA.instance.api.sqlany_error(@connection)
|
731
|
+
raise SQLAnywhereException.new(errstr, result, sql)
|
732
|
+
end
|
733
|
+
|
734
|
+
record = []
|
735
|
+
if( SA.instance.api.sqlany_num_cols(rs) > 0 )
|
736
|
+
while SA.instance.api.sqlany_fetch_next(rs) == 1
|
737
|
+
max_cols = SA.instance.api.sqlany_num_cols(rs)
|
738
|
+
result = Hash.new()
|
739
|
+
max_cols.times do |cols|
|
740
|
+
col_content=SA.instance.api.sqlany_get_column(rs, cols)[1]
|
741
|
+
if !col_content.nil? && col_content.is_a?(String)
|
742
|
+
puts ":encoding missing in database.yml" if ActiveRecord::Base.configurations[Rails.env]['encoding'].nil?
|
743
|
+
col_content = col_content.force_encoding(ActiveRecord::Base.configurations[Rails.env]['encoding'])
|
744
|
+
end
|
745
|
+
result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = col_content
|
746
|
+
end
|
747
|
+
record << result
|
748
|
+
end
|
749
|
+
@affected_rows = 0
|
750
|
+
else
|
751
|
+
@affected_rows = SA.instance.api.sqlany_affected_rows(rs)
|
752
|
+
end
|
753
|
+
SA.instance.api.sqlany_free_stmt(rs)
|
754
|
+
|
755
|
+
SA.instance.api.sqlany_commit(@connection) if @auto_commit
|
756
|
+
return record
|
757
|
+
end
|
758
|
+
|
759
|
+
# convert sqlany type to ruby type
|
760
|
+
# the types are taken from here
|
761
|
+
# http://dcx.sybase.com/1101/en/dbprogramming_en11/pg-c-api-native-type-enum.html
|
762
|
+
def native_type_to_ruby_type(native_type, value)
|
763
|
+
return nil if value.nil?
|
764
|
+
case native_type
|
765
|
+
when :decimal # (also and more importantly numeric)
|
766
|
+
BigDecimal.new(value)
|
767
|
+
when :var_char, :fix_char, :long_var_char, :string, :long_n_var_char
|
768
|
+
# hack, not sure how to manage proper encoding
|
769
|
+
value = value.force_encoding(ActiveRecord::Base.connection_config['encoding'] || 'UTF-8')
|
770
|
+
value = value.encode('UTF-8')
|
771
|
+
# Why am I removing the whitespace from the end of the string?
|
772
|
+
#
|
773
|
+
# Sqlanywhere allowed us to create a string foreign key.
|
774
|
+
# Somehow on only one end of the foreign key, the values got spaces at the end.
|
775
|
+
# The foreign key was still valid in Sqlanywhere: It worked for joins and it worked for referencing constraints.
|
776
|
+
#
|
777
|
+
# It however does not work for the ActiveRecord includes method.
|
778
|
+
# Rails will bring back the associated records, but then it fails to pair the records correctly.
|
779
|
+
# Removing whitespace from the ends of all strings fixes this. It is a hack however, so I'm open
|
780
|
+
# for suggestions on coming up with a better method.
|
781
|
+
#
|
782
|
+
begin
|
783
|
+
value = value.rstrip
|
784
|
+
rescue ArgumentError # invalid byte sequence in UTF-8
|
785
|
+
end
|
786
|
+
else
|
787
|
+
value
|
788
|
+
end
|
789
|
+
end
|
790
|
+
end
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# https://github.com/rsim/oracle-enhanced/blob/master/lib/activerecord-oracle_enhanced-adapter.rb
|
2
|
+
|
3
|
+
if defined?(::Rails::Railtie)
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
class SqlanywhereRailtie < ::Rails::Railtie
|
8
|
+
rake_tasks do
|
9
|
+
load 'active_record/connection_adapters/sqlanywhere.rake'
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Arel
|
2
|
+
module Visitors
|
3
|
+
class SQLAnywhere < Arel::Visitors::ToSql
|
4
|
+
private
|
5
|
+
def visit_Arel_Nodes_SelectStatement o
|
6
|
+
o = order_hacks(o)
|
7
|
+
|
8
|
+
is_distinct = using_distinct?(o)
|
9
|
+
|
10
|
+
o.limit = 1000000 if (o.offset && !o.limit)
|
11
|
+
o.limit = o.limit.expr if(o.limit.is_a?(Arel::Nodes::Limit))
|
12
|
+
o.limit = o.limit if(o.limit.is_a?(Fixnum))
|
13
|
+
|
14
|
+
[
|
15
|
+
"SELECT",
|
16
|
+
("DISTINCT" if is_distinct),
|
17
|
+
("TOP #{o.limit}" if o.limit),
|
18
|
+
(visit_Arel_Nodes_Offset(o.offset) if o.offset),
|
19
|
+
o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join,
|
20
|
+
("ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty?),
|
21
|
+
#("LIMIT #{o.limit}" if o.limit),
|
22
|
+
#(visit(o.offset) if o.offset),
|
23
|
+
(visit(o.lock) if o.lock),
|
24
|
+
].compact.join ' '
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_Arel_Nodes_SelectCore o
|
28
|
+
[
|
29
|
+
"#{o.projections.map { |x| visit x }.join ', '}",
|
30
|
+
("FROM #{visit o.source}" if o.source), # Joins
|
31
|
+
("WHERE #{o.wheres.map { |x| visit x }.join ' AND ' }" unless o.wheres.empty?),
|
32
|
+
("GROUP BY #{o.groups.map { |x| visit x }.join ', ' }" unless o.groups.empty?),
|
33
|
+
(visit(o.having) if o.having),
|
34
|
+
].compact.join ' '
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_Arel_Nodes_Group o
|
38
|
+
expr = o.expr.clone
|
39
|
+
if expr.class == Arel::Nodes::NamedFunction
|
40
|
+
expr.alias = nil
|
41
|
+
end
|
42
|
+
visit expr
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_Arel_Nodes_Offset o
|
46
|
+
"START AT #{visit(o.expr) + 1}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_Arel_Nodes_True o
|
50
|
+
"1=1"
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit_Arel_Nodes_False o
|
54
|
+
"1=0"
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_Arel_Nodes_Matches o
|
58
|
+
# The version in arel cannot like integer columns
|
59
|
+
left = visit o.left # This method sets last column
|
60
|
+
# If last column was left, visit o.right would return 0
|
61
|
+
self.last_column = nil
|
62
|
+
"#{left} LIKE #{visit o.right}"
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
def using_distinct?(o)
|
68
|
+
o.cores.any? do |core|
|
69
|
+
core.set_quantifier.class == Arel::Nodes::Distinct
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# The functions (order_hacks, split_order_string) are based on the Oracle Enhacned ActiveRecord driver maintained by Raimonds Simanovskis (2010)
|
74
|
+
# (https://github.com/rsim/oracle-enhanced)
|
75
|
+
|
76
|
+
###
|
77
|
+
# Hacks for the order clauses
|
78
|
+
def order_hacks o
|
79
|
+
return o if o.orders.empty?
|
80
|
+
return o unless o.cores.any? do |core|
|
81
|
+
core.projections.any? do |projection|
|
82
|
+
/DISTINCT.*FIRST_VALUE/ === projection
|
83
|
+
end
|
84
|
+
end
|
85
|
+
# Previous version with join and split broke ORDER BY clause
|
86
|
+
# if it contained functions with several arguments (separated by ',').
|
87
|
+
#
|
88
|
+
# orders = o.orders.map { |x| visit x }.join(', ').split(',')
|
89
|
+
orders = o.orders.map do |x|
|
90
|
+
string = visit x
|
91
|
+
if string.include?(',')
|
92
|
+
split_order_string(string)
|
93
|
+
else
|
94
|
+
string
|
95
|
+
end
|
96
|
+
end.flatten
|
97
|
+
o.orders = []
|
98
|
+
orders.each_with_index do |order, i|
|
99
|
+
o.orders <<
|
100
|
+
Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
|
101
|
+
end
|
102
|
+
o
|
103
|
+
end
|
104
|
+
|
105
|
+
# Split string by commas but count opening and closing brackets
|
106
|
+
# and ignore commas inside brackets.
|
107
|
+
def split_order_string(string)
|
108
|
+
array = []
|
109
|
+
i = 0
|
110
|
+
string.split(',').each do |part|
|
111
|
+
if array[i]
|
112
|
+
array[i] << ',' << part
|
113
|
+
else
|
114
|
+
# to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
|
115
|
+
array[i] = '' << part
|
116
|
+
end
|
117
|
+
i += 1 if array[i].count('(') == array[i].count(')')
|
118
|
+
end
|
119
|
+
array
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
Arel::Visitors::VISITORS['sqlanywhere'] = Arel::Visitors::SQLAnywhere
|
data/test/connection.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
print "Using native SQLAnywhere Interface\n"
|
2
|
+
require_dependency 'models/course'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
6
|
+
|
7
|
+
ActiveRecord::Base.configurations = {
|
8
|
+
'arunit' => {
|
9
|
+
:adapter => 'sqlanywhere',
|
10
|
+
:database => 'arunit',
|
11
|
+
:server => 'arunit',
|
12
|
+
:username => 'dba',
|
13
|
+
:password => 'sql'
|
14
|
+
},
|
15
|
+
'arunit2' => {
|
16
|
+
:adapter => 'sqlanywhere',
|
17
|
+
:database => 'arunit2',
|
18
|
+
:server => 'arunit',
|
19
|
+
:username => 'dba',
|
20
|
+
:password => 'sql'
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
ActiveRecord::Base.establish_connection 'arunit'
|
25
|
+
Course.establish_connection 'arunit2'
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-sqlanywhere-adapter-in4systems
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Farar
|
9
|
+
- Chris Couzens
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-01-21 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sqlanywhere-ffi
|
17
|
+
version_requirements: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
none: false
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 1.0.0
|
28
|
+
none: false
|
29
|
+
prerelease: false
|
30
|
+
type: :runtime
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: activerecord
|
33
|
+
version_requirements: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.0.3
|
38
|
+
none: false
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.0.3
|
44
|
+
none: false
|
45
|
+
prerelease: false
|
46
|
+
type: :runtime
|
47
|
+
description: ActiveRecord driver for SQL Anywhere customized for in4systems
|
48
|
+
email: eric.farrar@ianywhere.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- CHANGELOG
|
54
|
+
- LICENSE
|
55
|
+
- README
|
56
|
+
- test/connection.rb
|
57
|
+
- lib/active_record/connection_adapters/sqlanywhere_adapter.rb
|
58
|
+
- lib/arel/visitors/sqlanywhere.rb
|
59
|
+
- lib/active_record/connection_adapters/sqlanywhere.rake
|
60
|
+
- lib/activerecord-sqlanywhere-adapter.rb
|
61
|
+
homepage: https://github.com/in4systems/activerecord-sqlanywhere-adapter
|
62
|
+
licenses:
|
63
|
+
- Apache License Version 2.0
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: !binary |-
|
73
|
+
MA==
|
74
|
+
none: false
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: !binary |-
|
80
|
+
MA==
|
81
|
+
none: false
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.8.24
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: ActiveRecord driver for SQL Anywhere
|
88
|
+
test_files: []
|