@@ -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
591766end #module ActiveRecord
0 commit comments