activerecord-sqlanywhere-adapter-in4systems 1.0.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 +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: []
|