Skip to content

Commit d0fd3af

Browse files
committed
Mass changes to support SQL Server 2005
1 parent 670f255 commit d0fd3af

File tree

1 file changed

+219
-44
lines changed

1 file changed

+219
-44
lines changed

sqlserver_adapter_rails.rb

Lines changed: 219 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,43 @@ def self.sqlserver_connection(config) #:nodoc:
4747
conn["AutoCommit"] = autocommit
4848
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
4949
end
50+
# Overridden to include support for SQL server's lack of = operator on
51+
# text/ntext/image columns LIKE operator is used instead
52+
def self.sanitize_sql_hash(attrs)
53+
conditions = attrs.map do |attr, value|
54+
col = self.columns.find {|c| c.name == attr}
55+
if col && col.respond_to?("is_special") && col.is_special
56+
"#{table_name}.#{connection.quote_column_name(attr)} LIKE ?"
57+
else
58+
"#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
59+
end
60+
end.join(' AND ')
61+
replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
62+
end
63+
64+
# In the case of SQL server, the lock value must follow the FROM clause
65+
def self.construct_finder_sql(options)
66+
scope = scope(:find)
67+
sql = "SELECT #{(scope && scope[:select]) || options[:select] || '*'} "
68+
sql << "FROM #{(scope && scope[:from]) || options[:from] || table_name} "
69+
70+
if ActiveRecord::Base.connection.adapter_name == "SQLServer" && !options[:lock].blank? # SQLServer
71+
add_lock!(sql, options, scope)
72+
end
73+
74+
add_joins!(sql, options, scope)
75+
add_conditions!(sql, options[:conditions], scope)
76+
77+
sql << " GROUP BY #{options[:group]} " if options[:group]
78+
79+
add_order!(sql, options[:order], scope)
80+
add_limit!(sql, options, scope)
81+
add_lock!(sql, options, scope) unless ActiveRecord::Base.connection.adapter_name == "SQLServer" # SQLServer
82+
# $log.debug "database_helper: construct_finder_sql: sql at end: #{sql.inspect}"
83+
sql
84+
end
85+
86+
5087
end # class Base
5188

5289
module ConnectionAdapters
@@ -121,26 +158,18 @@ def self.string_to_time(value)
121158

122159
# These methods will only allow the adapter to insert binary data with a length of 7K or less
123160
# because of a SQL Server statement length policy.
161+
# Convert strings to hex before storing in the database
124162
def self.string_to_binary(value)
125-
value.gsub(/(\r|\n|\0|\x1a)/) do
126-
case $1
127-
when "\r" then "%00"
128-
when "\n" then "%01"
129-
when "\0" then "%02"
130-
when "\x1a" then "%03"
131-
end
132-
end
163+
"0x#{value.unpack("H*")[0]}"
133164
end
134165

135166
def self.binary_to_string(value)
136-
value.gsub(/(%00|%01|%02|%03)/) do
137-
case $1
138-
when "%00" then "\r"
139-
when "%01" then "\n"
140-
when "%02\0" then "\0"
141-
when "%03" then "\x1a"
142-
end
143-
end
167+
# TODO: Need to remove conditional pack (should always have to pack hex characters into blob)
168+
# Assigning a value to a binary column causes the string_to_binary to hexify it
169+
# This hex value is stored in the DB but the original value is retained in the
170+
# cache. By forcing reload, the value coming into binary_to_string will always
171+
# be hex. Need to force reload or update the cached column's value to match what is sent to the DB.
172+
value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
144173
end
145174
end
146175

@@ -186,24 +215,28 @@ def self.binary_to_string(value)
186215
# [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux]
187216
class SQLServerAdapter < AbstractAdapter
188217

218+
# add synchronization to adapter to prevent 'invalid cursor state' error
219+
require 'sync'
189220
def initialize(connection, logger, connection_options=nil)
190221
super(connection, logger)
191222
@connection_options = connection_options
223+
@sql_connection_lock = Sync.new
192224
end
193225

226+
# change defaults for text and binary to use varchar(max) and varbinary(max) types
194227
def native_database_types
195228
{
196229
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
197230
:string => { :name => "varchar", :limit => 255 },
198-
:text => { :name => "text" },
231+
:text => { :name => "varchar(max)" },
199232
:integer => { :name => "int" },
200233
:float => { :name => "float", :limit => 8 },
201234
:decimal => { :name => "decimal" },
202235
:datetime => { :name => "datetime" },
203236
:timestamp => { :name => "datetime" },
204237
:time => { :name => "datetime" },
205238
:date => { :name => "datetime" },
206-
:binary => { :name => "image"},
239+
:binary => { :name => "varbinary(max)"},
207240
:boolean => { :name => "bit"}
208241
}
209242
end
@@ -216,7 +249,10 @@ def supports_migrations? #:nodoc:
216249
true
217250
end
218251

252+
# Set limit to nil if text or binary due to issues passing the limit on these types in SQL Server
253+
# SQL server complains about the data exceeding 8000 bytes
219254
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
255+
limit = nil if %w{text binary}.include?(type.to_s)
220256
return super unless type.to_s == 'integer'
221257

222258
if limit.nil? || limit == 4
@@ -250,14 +286,46 @@ def reconnect!
250286
# Disconnects from the database
251287

252288
def disconnect!
253-
@connection.disconnect rescue nil
289+
@sql_connection_lock.synchronize(:EX) do
290+
begin
291+
@connection.disconnect
292+
rescue nil
293+
end
294+
end
254295
end
255296

297+
# Add synchronization for the db connection to ensure no one else is using this one
298+
# prevents 'invalid cursor state' error
299+
def select_rows(sql, name = nil)
300+
rows = []
301+
repair_special_columns(sql)
302+
log(sql, name) do
303+
@sql_connection_lock.synchronize(:EX) do
304+
@connection.select_all(sql) do |row|
305+
record = []
306+
row.each do |col|
307+
if col.is_a? DBI::Timestamp
308+
record << col.to_time
309+
else
310+
record << col
311+
end
312+
end
313+
rows << record
314+
end
315+
end
316+
end
317+
rows
318+
end
319+
320+
# Add synchronization for the db connection to ensure no one else is using this one
321+
# prevents 'invalid cursor state' error
256322
def columns(table_name, name = nil)
257323
return [] if table_name.blank?
258324
table_name = table_name.to_s if table_name.is_a?(Symbol)
259325
table_name = table_name.split('.')[-1] unless table_name.nil?
260326
table_name = table_name.gsub(/[\[\]]/, '')
327+
# Added code to handle varchar(max), varbinary(max), nvarchar(max) returning a length of -1
328+
# this manifested itself in session data since Rails believed the column was too small
261329
sql = %Q{
262330
SELECT
263331
cols.COLUMN_NAME as ColName,
@@ -266,7 +334,10 @@ def columns(table_name, name = nil)
266334
cols.NUMERIC_PRECISION as numeric_precision,
267335
cols.DATA_TYPE as ColType,
268336
cols.IS_NULLABLE As IsNullable,
269-
COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) as Length,
337+
CASE
338+
WHEN cols.DATA_TYPE IN ('varchar', 'nvarchar', 'varbinary') AND COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) = -1 THEN 2147483648
339+
ELSE COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME)
340+
END as Length,
270341
COLUMNPROPERTY(OBJECT_ID(cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') as IsIdentity,
271342
cols.NUMERIC_SCALE as Scale
272343
FROM INFORMATION_SCHEMA.COLUMNS cols
@@ -275,15 +346,18 @@ def columns(table_name, name = nil)
275346
# Comment out if you want to have the Columns select statment logged.
276347
# Personally, I think it adds unnecessary bloat to the log.
277348
# If you do comment it out, make sure to un-comment the "result" line that follows
278-
result = log(sql, name) { @connection.select_all(sql) }
279-
#result = @connection.select_all(sql)
280-
columns = []
281-
result.each do |field|
282-
default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue]
283-
if field[:ColType] =~ /numeric|decimal/i
284-
type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})"
285-
else
286-
type = "#{field[:ColType]}(#{field[:Length]})"
349+
result = log(sql, name) do
350+
@sql_connection_lock.synchronize(:EX) do
351+
@connection.select_all(sql)
352+
end
353+
end
354+
columns = []
355+
result.each do |field|
356+
default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/i ? nil : field[:DefaultValue]
357+
if field[:ColType] =~ /numeric|decimal/i
358+
type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})"
359+
else
360+
type = "#{field[:ColType]}(#{field[:Length]})"
287361
end
288362
is_identity = field[:IsIdentity] == 1
289363
is_nullable = field[:IsNullable] == 'YES'
@@ -305,40 +379,59 @@ def update(sql, name = nil)
305379

306380
alias_method :delete, :update
307381

382+
# override execute to synchronize the connection
308383
def execute(sql, name = nil)
309384
if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql))
310385
log(sql, name) do
311386
with_identity_insert_enabled(table_name) do
312-
@connection.execute(sql) do |handle|
313-
yield(handle) if block_given?
387+
@sql_connection_lock.synchronize(:EX) do
388+
@connection.execute(sql) do |handle|
389+
yield(handle) if block_given?
390+
end
314391
end
315392
end
316393
end
317394
else
318395
log(sql, name) do
319-
@connection.execute(sql) do |handle|
320-
yield(handle) if block_given?
396+
@sql_connection_lock.synchronize(:EX) do
397+
@connection.execute(sql) do |handle|
398+
yield(handle) if block_given?
399+
end
321400
end
322401
end
323402
end
324403
end
325404

405+
406+
# Add synchronization for the db connection to ensure no one else is using this one
407+
# prevents 'Could not change transaction status' error
326408
def begin_db_transaction
327-
@connection["AutoCommit"] = false
328-
rescue Exception => e
329-
@connection["AutoCommit"] = true
409+
@sql_connection_lock.synchronize(:EX) do
410+
begin
411+
@connection["AutoCommit"] = false
412+
rescue Exception => e
413+
@connection["AutoCommit"] = true
414+
end
415+
end
330416
end
331-
332417
def commit_db_transaction
333-
@connection.commit
334-
ensure
335-
@connection["AutoCommit"] = true
418+
@sql_connection_lock.synchronize(:EX) do
419+
begin
420+
@connection.commit
421+
ensure
422+
@connection["AutoCommit"] = true
423+
end
424+
end
336425
end
337426

338427
def rollback_db_transaction
339-
@connection.rollback
340-
ensure
341-
@connection["AutoCommit"] = true
428+
@sql_connection_lock.synchronize(:EX) do
429+
begin
430+
@connection.rollback
431+
ensure
432+
@connection["AutoCommit"] = true
433+
end
434+
end
342435
end
343436

344437
def quote(value, column = nil)
@@ -457,8 +550,59 @@ def rename_column(table, column, new_column_name)
457550
execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'"
458551
end
459552

553+
# database_statements line 108 Set the SQL specific rowlocking
554+
# was previously generating invalid syntax for SQL server
555+
def add_lock!(sql, options)
556+
case lock = options[:lock]
557+
when true then sql << "WITH(HOLDLOCK, ROWLOCK) "
558+
when String then sql << "#{lock} "
559+
end
560+
end
561+
562+
563+
564+
# Delete the default options if it's nil. Adapter was adding default NULL contraints
565+
# to all columns which caused problems when trying to alter the column
566+
def add_column_options!(sql, options) #:nodoc:
567+
options.delete(:default) if options[:default].nil?
568+
super
569+
end
570+
571+
# calculate column size to fix issue
572+
# size XXXXX given to the column 'data' exceeds the maximum allowed for any data type (8000)
573+
def column_total_size(table_name)
574+
return nil if table_name.blank?
575+
table_name = table_name.to_s if table_name.is_a?(Symbol)
576+
table_name = table_name.split('.')[-1] unless table_name.nil?
577+
table_name = table_name.gsub(/[\[\]]/, '')
578+
sql = %Q{
579+
SELECT SUM(COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME)) as Length
580+
FROM INFORMATION_SCHEMA.COLUMNS cols
581+
WHERE cols.TABLE_NAME = '#{table_name}'
582+
}
583+
# Comment out if you want to have the Columns select statment logged.
584+
# Personally, I think it adds unnecessary bloat to the log. If you do
585+
# comment it out, make sure to un-comment the "result" line that follows
586+
result = log(sql, name) do
587+
@sql_connection_lock.synchronize(:EX) { @connection.select_all(sql) }
588+
end
589+
field[:Length].to_i
590+
end
591+
# if binary, calculate te the remaining amount for size
592+
# issue: size XXXXX given to the column 'data' exceeds the maximum allowed for any data type (8000)
460593
def change_column(table_name, column_name, type, options = {}) #:nodoc:
461-
sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"]
594+
# $log.debug "change_column"
595+
sql_commands = []
596+
597+
# Handle conversion of text columns to binary columns by first
598+
# converting to varchar. We determine the amount of space left for the
599+
# columns so we can get the most out of the conversion.
600+
if type == :binary
601+
col = self.columns(table_name, column_name)
602+
sql_commands << "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(:string, 8000 - column_total_size(table_name))}" if col && col.type == :text
603+
end
604+
605+
sql_commands << "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
462606
if options_include_default?(options)
463607
remove_default_constraint(table_name, column_name)
464608
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}"
@@ -587,5 +731,36 @@ def repair_special_columns(sql)
587731
end
588732

589733
end #class SQLServerAdapter < AbstractAdapter
734+
# If value is a string and destination column is binary, don't quote the string for MS SQL
735+
module Quoting
736+
# Quotes the column value to help prevent
737+
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
738+
def quote(value, column = nil)
739+
# records are quoted as their primary key
740+
return value.quoted_id if value.respond_to?(:quoted_id)
741+
# puts "Type: #{column.type} Name: #{column.name}" if column
742+
case value
743+
when String, ActiveSupport::Multibyte::Chars
744+
value = value.to_s
745+
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
746+
column.class.string_to_binary(value)
747+
elsif column && [:integer, :float].include?(column.type)
748+
value = column.type == :integer ? value.to_i : value.to_f
749+
value.to_s
750+
else
751+
"'#{quote_string(value)}'" # ' (for ruby-mode)
752+
end
753+
when NilClass then "NULL"
754+
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
755+
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
756+
when Float, Fixnum, Bignum then value.to_s
757+
# BigDecimals need to be output in a non-normalized form and quoted.
758+
when BigDecimal then value.to_s('F')
759+
when Date then "'#{value.to_s}'"
760+
when Time, DateTime then "'#{quoted_date(value)}'"
761+
else "'#{quote_string(value.to_yaml)}'"
762+
end
763+
end
764+
end
590765
end #module ConnectionAdapters
591766
end #module ActiveRecord

0 commit comments

Comments
 (0)