rails-sqlserver-2000-2005-adapter 1.0.0
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 +25 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +0 -0
- data/RUNNING_UNIT_TESTS +60 -0
- data/Rakefile +95 -0
- data/autotest/discover.rb +4 -0
- data/autotest/railssqlserver.rb +16 -0
- data/autotest/sqlserver.rb +54 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +913 -0
- data/lib/core_ext/active_record.rb +71 -0
- data/lib/core_ext/dbi.rb +83 -0
- data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
- data/test/cases/adapter_test_sqlserver.rb +428 -0
- data/test/cases/basics_test_sqlserver.rb +21 -0
- data/test/cases/calculations_test_sqlserver.rb +20 -0
- data/test/cases/column_test_sqlserver.rb +66 -0
- data/test/cases/connection_test_sqlserver.rb +103 -0
- data/test/cases/eager_association_test_sqlserver.rb +22 -0
- data/test/cases/inheritance_test_sqlserver.rb +28 -0
- data/test/cases/migration_test_sqlserver.rb +57 -0
- data/test/cases/offset_and_limit_test_sqlserver.rb +82 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +100 -0
- data/test/cases/query_cache_test_sqlserver.rb +24 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +40 -0
- data/test/cases/specific_schema_test_sqlserver.rb +25 -0
- data/test/cases/sqlserver_helper.rb +88 -0
- data/test/connections/native_sqlserver/connection.rb +23 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/schema/sqlserver_specific_schema.rb +38 -0
- metadata +96 -0
@@ -0,0 +1,913 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
require_library_or_gem 'dbi' unless defined?(DBI)
|
3
|
+
require 'core_ext/dbi'
|
4
|
+
require 'core_ext/active_record'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
class Base
|
10
|
+
|
11
|
+
def self.sqlserver_connection(config) #:nodoc:
|
12
|
+
config.symbolize_keys!
|
13
|
+
mode = config[:mode] ? config[:mode].to_s.upcase : 'ADO'
|
14
|
+
username = config[:username] ? config[:username].to_s : 'sa'
|
15
|
+
password = config[:password] ? config[:password].to_s : ''
|
16
|
+
if mode == "ODBC"
|
17
|
+
raise ArgumentError, "Missing DSN. Argument ':dsn' must be set in order for this adapter to work." unless config.has_key?(:dsn)
|
18
|
+
dsn = config[:dsn]
|
19
|
+
driver_url = "DBI:ODBC:#{dsn}"
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
|
22
|
+
database = config[:database]
|
23
|
+
host = config[:host] ? config[:host].to_s : 'localhost'
|
24
|
+
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
|
25
|
+
end
|
26
|
+
conn = DBI.connect(driver_url, username, password)
|
27
|
+
conn["AutoCommit"] = true
|
28
|
+
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module ConnectionAdapters
|
34
|
+
|
35
|
+
class SQLServerColumn < Column
|
36
|
+
|
37
|
+
def initialize(name, default, sql_type = nil, null = true, sqlserver_options = {})
|
38
|
+
@sqlserver_options = sqlserver_options
|
39
|
+
super(name, default, sql_type, null)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
def string_to_binary(value)
|
45
|
+
"0x#{value.unpack("H*")[0]}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def binary_to_string(value)
|
49
|
+
value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_identity?
|
55
|
+
@sqlserver_options[:is_identity]
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_special?
|
59
|
+
# TODO: Not sure if these should be added: varbinary(max), nchar, nvarchar(max)
|
60
|
+
sql_type =~ /^text|ntext|image$/
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_utf8?
|
64
|
+
sql_type =~ /nvarchar|ntext|nchar|nvarchar(max)/i
|
65
|
+
end
|
66
|
+
|
67
|
+
def table_name
|
68
|
+
@sqlserver_options[:table_name]
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_klass
|
72
|
+
@table_klass ||= table_name.classify.constantize rescue nil
|
73
|
+
(@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def extract_limit(sql_type)
|
79
|
+
case sql_type
|
80
|
+
when /^smallint/i then 2
|
81
|
+
when /^int/i then 4
|
82
|
+
when /^bigint/i then 8
|
83
|
+
else super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def simplified_type(field_type)
|
88
|
+
case field_type
|
89
|
+
when /real/i then :float
|
90
|
+
when /money/i then :decimal
|
91
|
+
when /image/i then :binary
|
92
|
+
when /bit/i then :boolean
|
93
|
+
when /uniqueidentifier/i then :string
|
94
|
+
when /datetime/i then simplified_datetime
|
95
|
+
else super
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def simplified_datetime
|
100
|
+
if table_klass && table_klass.coerced_sqlserver_date_columns.include?(name)
|
101
|
+
:date
|
102
|
+
elsif table_klass && table_klass.coerced_sqlserver_time_columns.include?(name)
|
103
|
+
:time
|
104
|
+
else
|
105
|
+
:datetime
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end #SQLServerColumn
|
110
|
+
|
111
|
+
# In ADO mode, this adapter will ONLY work on Windows systems,
|
112
|
+
# since it relies on Win32OLE, which, to my knowledge, is only
|
113
|
+
# available on Windows.
|
114
|
+
#
|
115
|
+
# This mode also relies on the ADO support in the DBI module. If you are using the
|
116
|
+
# one-click installer of Ruby, then you already have DBI installed, but
|
117
|
+
# the ADO module is *NOT* installed. You will need to get the latest
|
118
|
+
# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
|
119
|
+
# unzip it, and copy the file
|
120
|
+
# <tt>src/lib/dbd_ado/ADO.rb</tt>
|
121
|
+
# to
|
122
|
+
# <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
|
123
|
+
# (you will more than likely need to create the ADO directory).
|
124
|
+
# Once you've installed that file, you are ready to go.
|
125
|
+
#
|
126
|
+
# In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
|
127
|
+
# the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
|
128
|
+
# and it is available at http://www.ch-werner.de/rubyodbc/
|
129
|
+
#
|
130
|
+
# Options:
|
131
|
+
#
|
132
|
+
# * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
|
133
|
+
# * <tt>:username</tt> -- Defaults to sa.
|
134
|
+
# * <tt>:password</tt> -- Defaults to empty string.
|
135
|
+
# * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
|
136
|
+
#
|
137
|
+
# ADO specific options:
|
138
|
+
#
|
139
|
+
# * <tt>:host</tt> -- Defaults to localhost.
|
140
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
141
|
+
# * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
|
142
|
+
#
|
143
|
+
# ODBC specific options:
|
144
|
+
#
|
145
|
+
# * <tt>:dsn</tt> -- Defaults to nothing.
|
146
|
+
#
|
147
|
+
# ADO code tested on Windows 2000 and higher systems,
|
148
|
+
# running ruby 1.8.2 (2004-07-29) [i386-mswin32], and SQL Server 2000 SP3.
|
149
|
+
#
|
150
|
+
# ODBC code tested on a Fedora Core 4 system, running FreeTDS 0.63,
|
151
|
+
# unixODBC 2.2.11, Ruby ODBC 0.996, Ruby DBI 0.0.23 and Ruby 1.8.2.
|
152
|
+
# [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux]
|
153
|
+
class SQLServerAdapter < AbstractAdapter
|
154
|
+
|
155
|
+
ADAPTER_NAME = 'SQLServer'.freeze
|
156
|
+
DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
|
157
|
+
SUPPORTED_VERSIONS = [2000,2005].freeze
|
158
|
+
LIMITABLE_TYPES = [:string,:integer,:float].freeze
|
159
|
+
|
160
|
+
cattr_accessor :native_text_database_type
|
161
|
+
|
162
|
+
class << self
|
163
|
+
|
164
|
+
def type_limitable?(type)
|
165
|
+
LIMITABLE_TYPES.include?(type.to_sym)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def initialize(connection, logger, connection_options=nil)
|
171
|
+
super(connection, logger)
|
172
|
+
@connection_options = connection_options
|
173
|
+
@sqlserver_columns_cache = {}
|
174
|
+
unless SUPPORTED_VERSIONS.include?(database_year)
|
175
|
+
raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# ABSTRACT ADAPTER =========================================#
|
180
|
+
|
181
|
+
def adapter_name
|
182
|
+
ADAPTER_NAME
|
183
|
+
end
|
184
|
+
|
185
|
+
def supports_migrations?
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
def supports_ddl_transactions?
|
190
|
+
true
|
191
|
+
end
|
192
|
+
|
193
|
+
def database_version
|
194
|
+
@database_version ||= select_value('SELECT @@version')
|
195
|
+
end
|
196
|
+
|
197
|
+
def database_year
|
198
|
+
DATABASE_VERSION_REGEXP.match(database_version)[1].to_i
|
199
|
+
end
|
200
|
+
|
201
|
+
def sqlserver_2000?
|
202
|
+
database_year == 2000
|
203
|
+
end
|
204
|
+
|
205
|
+
def sqlserver_2005?
|
206
|
+
database_year == 2005
|
207
|
+
end
|
208
|
+
|
209
|
+
def inspect
|
210
|
+
"#<#{self.class} year: #{database_year}, connection_options: #{@connection_options.inspect}>"
|
211
|
+
end
|
212
|
+
|
213
|
+
def native_text_database_type
|
214
|
+
self.class.native_text_database_type || (sqlserver_2005? ? 'varchar(max)' : 'text')
|
215
|
+
end
|
216
|
+
|
217
|
+
# QUOTING ==================================================#
|
218
|
+
|
219
|
+
def quote(value, column = nil)
|
220
|
+
if value.kind_of?(String) && column && column.type == :binary
|
221
|
+
column.class.string_to_binary(value)
|
222
|
+
else
|
223
|
+
super
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def quote_string(string)
|
228
|
+
string.to_s.gsub(/\'/, "''")
|
229
|
+
end
|
230
|
+
|
231
|
+
def quote_column_name(column_name)
|
232
|
+
column_name.to_s.split('.').map{ |name| "[#{name}]" }.join('.')
|
233
|
+
end
|
234
|
+
|
235
|
+
def quoted_true
|
236
|
+
'1'
|
237
|
+
end
|
238
|
+
|
239
|
+
def quoted_false
|
240
|
+
'0'
|
241
|
+
end
|
242
|
+
|
243
|
+
def quoted_date(value)
|
244
|
+
if value.acts_like?(:time) && value.respond_to?(:usec)
|
245
|
+
"#{super}.#{sprintf("%06d",value.usec)[0..2]}"
|
246
|
+
else
|
247
|
+
super
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# REFERENTIAL INTEGRITY ====================================#
|
252
|
+
|
253
|
+
def disable_referential_integrity(&block)
|
254
|
+
do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
|
255
|
+
yield
|
256
|
+
ensure
|
257
|
+
do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'"
|
258
|
+
end
|
259
|
+
|
260
|
+
# CONNECTION MANAGEMENT ====================================#
|
261
|
+
|
262
|
+
def active?
|
263
|
+
raw_connection.execute("SELECT 1").finish
|
264
|
+
true
|
265
|
+
rescue DBI::DatabaseError, DBI::InterfaceError
|
266
|
+
false
|
267
|
+
end
|
268
|
+
|
269
|
+
def reconnect!
|
270
|
+
disconnect!
|
271
|
+
@connection = DBI.connect(*@connection_options)
|
272
|
+
rescue DBI::DatabaseError => e
|
273
|
+
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
|
274
|
+
false
|
275
|
+
end
|
276
|
+
|
277
|
+
def disconnect!
|
278
|
+
raw_connection.disconnect rescue nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def finish_statement_handle(handle)
|
282
|
+
handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
|
283
|
+
handle
|
284
|
+
end
|
285
|
+
|
286
|
+
# DATABASE STATEMENTS ======================================#
|
287
|
+
|
288
|
+
def select_rows(sql, name = nil)
|
289
|
+
raw_select(sql,name).last
|
290
|
+
end
|
291
|
+
|
292
|
+
def execute(sql, name = nil, &block)
|
293
|
+
if table_name = query_requires_identity_insert?(sql)
|
294
|
+
handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
|
295
|
+
else
|
296
|
+
handle = raw_execute(sql,name,&block)
|
297
|
+
end
|
298
|
+
finish_statement_handle(handle)
|
299
|
+
end
|
300
|
+
|
301
|
+
def begin_db_transaction
|
302
|
+
do_execute "BEGIN TRANSACTION"
|
303
|
+
end
|
304
|
+
|
305
|
+
def commit_db_transaction
|
306
|
+
do_execute "COMMIT TRANSACTION"
|
307
|
+
end
|
308
|
+
|
309
|
+
def rollback_db_transaction
|
310
|
+
do_execute "ROLLBACK TRANSACTION" rescue nil
|
311
|
+
end
|
312
|
+
|
313
|
+
def add_limit_offset!(sql, options)
|
314
|
+
# Validate and/or convert integers for :limit and :offets options.
|
315
|
+
if options[:offset]
|
316
|
+
raise ArgumentError, "offset should have a limit" unless options[:limit]
|
317
|
+
unless options[:offset].kind_of?(Integer)
|
318
|
+
if options[:offset] =~ /^\d+$/
|
319
|
+
options[:offset] = options[:offset].to_i
|
320
|
+
else
|
321
|
+
raise ArgumentError, "offset should be an integer"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
if options[:limit] && !(options[:limit].kind_of?(Integer))
|
326
|
+
if options[:limit] =~ /^\d+$/
|
327
|
+
options[:limit] = options[:limit].to_i
|
328
|
+
else
|
329
|
+
raise ArgumentError, "limit should be an integer"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
# The business of adding limit/offset
|
333
|
+
if options[:limit] and options[:offset]
|
334
|
+
total_rows = select_value("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally").to_i
|
335
|
+
if (options[:limit] + options[:offset]) >= total_rows
|
336
|
+
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
|
337
|
+
end
|
338
|
+
# Make sure we do not need a special limit/offset for association limiting. http://gist.github.com/25118
|
339
|
+
add_limit_offset_for_association_limiting!(sql,options) and return if sql_for_association_limiting?(sql)
|
340
|
+
# Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
|
341
|
+
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
|
342
|
+
sql << ") AS tmp1"
|
343
|
+
if options[:order]
|
344
|
+
order = options[:order].split(',').map do |field|
|
345
|
+
order_by_column, order_direction = field.split(" ")
|
346
|
+
order_by_column = quote_column_name(order_by_column)
|
347
|
+
# Investigate the SQL query to figure out if the order_by_column has been renamed.
|
348
|
+
if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
|
349
|
+
# Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
|
350
|
+
order_by_column = $1
|
351
|
+
elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
|
352
|
+
order_by_column = $1
|
353
|
+
else
|
354
|
+
# It doesn't appear that the column name has been renamed as part of the query. Use just the column
|
355
|
+
# name rather than the full identifier for the outer queries.
|
356
|
+
order_by_column = order_by_column.split('.').last
|
357
|
+
end
|
358
|
+
# Put the column name and eventual direction back together
|
359
|
+
[order_by_column, order_direction].join(' ').strip
|
360
|
+
end.join(', ')
|
361
|
+
sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
|
362
|
+
else
|
363
|
+
sql << ") AS tmp2"
|
364
|
+
end
|
365
|
+
elsif options[:limit] && sql !~ /^\s*SELECT (@@|COUNT\()/i
|
366
|
+
if md = sql.match(/^(\s*SELECT)(\s+DISTINCT)?(.*)/im)
|
367
|
+
sql.replace "#{md[1]}#{md[2]} TOP #{options[:limit]}#{md[3]}"
|
368
|
+
else
|
369
|
+
# Account for building SQL fragments without SELECT yet. See #update_all and #limited_update_conditions.
|
370
|
+
sql.replace "TOP #{options[:limit]} #{sql}"
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def add_lock!(sql, options)
|
376
|
+
# http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
|
377
|
+
return unless options[:lock]
|
378
|
+
lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
|
379
|
+
from_table = sql.match(/FROM(.*)WHERE/im)[1]
|
380
|
+
sql.sub! from_table, "#{from_table}#{lock_type} "
|
381
|
+
end
|
382
|
+
|
383
|
+
def empty_insert_statement(table_name)
|
384
|
+
"INSERT INTO #{quote_table_name(table_name)} DEFAULT VALUES"
|
385
|
+
end
|
386
|
+
|
387
|
+
def case_sensitive_equality_operator
|
388
|
+
"COLLATE Latin1_General_CS_AS ="
|
389
|
+
end
|
390
|
+
|
391
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
392
|
+
match_data = where_sql.match(/(.*)WHERE/)
|
393
|
+
limit = match_data[1]
|
394
|
+
where_sql.sub!(limit,'')
|
395
|
+
"WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
396
|
+
end
|
397
|
+
|
398
|
+
# SCHEMA STATEMENTS ========================================#
|
399
|
+
|
400
|
+
def native_database_types
|
401
|
+
binary = sqlserver_2005? ? "varbinary(max)" : "image"
|
402
|
+
{
|
403
|
+
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
|
404
|
+
:string => { :name => "varchar", :limit => 255 },
|
405
|
+
:text => { :name => native_text_database_type },
|
406
|
+
:integer => { :name => "int", :limit => 4 },
|
407
|
+
:float => { :name => "float", :limit => 8 },
|
408
|
+
:decimal => { :name => "decimal" },
|
409
|
+
:datetime => { :name => "datetime" },
|
410
|
+
:timestamp => { :name => "datetime" },
|
411
|
+
:time => { :name => "datetime" },
|
412
|
+
:date => { :name => "datetime" },
|
413
|
+
:binary => { :name => binary },
|
414
|
+
:boolean => { :name => "bit"}
|
415
|
+
}
|
416
|
+
end
|
417
|
+
|
418
|
+
def table_alias_length
|
419
|
+
128
|
420
|
+
end
|
421
|
+
|
422
|
+
def tables(name = nil)
|
423
|
+
select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties'"
|
424
|
+
end
|
425
|
+
|
426
|
+
def indexes(table_name, name = nil)
|
427
|
+
select("EXEC sp_helpindex #{quote_table_name(table_name)}",name).inject([]) do |indexes,index|
|
428
|
+
if index['index_description'] =~ /primary key/
|
429
|
+
indexes
|
430
|
+
else
|
431
|
+
name = index['index_name']
|
432
|
+
unique = index['index_description'] =~ /unique/
|
433
|
+
columns = index['index_keys'].split(',').map do |column|
|
434
|
+
column.strip!
|
435
|
+
column.gsub! '(-)', '' if column.ends_with?('(-)')
|
436
|
+
column
|
437
|
+
end
|
438
|
+
indexes << IndexDefinition.new(table_name, name, unique, columns)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def columns(table_name, name = nil)
|
444
|
+
return [] if table_name.blank?
|
445
|
+
cache_key = unqualify_table_name(table_name)
|
446
|
+
@sqlserver_columns_cache[cache_key] ||= column_definitions(table_name).collect do |ci|
|
447
|
+
sqlserver_options = ci.except(:name,:default_value,:type,:null)
|
448
|
+
SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def create_table(table_name, options = {})
|
453
|
+
super
|
454
|
+
remove_sqlserver_columns_cache_for(table_name)
|
455
|
+
end
|
456
|
+
|
457
|
+
def rename_table(table_name, new_name)
|
458
|
+
do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
|
459
|
+
end
|
460
|
+
|
461
|
+
def drop_table(table_name, options = {})
|
462
|
+
super
|
463
|
+
remove_sqlserver_columns_cache_for(table_name)
|
464
|
+
end
|
465
|
+
|
466
|
+
def add_column(table_name, column_name, type, options = {})
|
467
|
+
super
|
468
|
+
remove_sqlserver_columns_cache_for(table_name)
|
469
|
+
end
|
470
|
+
|
471
|
+
def remove_column(table_name, *column_names)
|
472
|
+
column_names.flatten.each do |column_name|
|
473
|
+
remove_check_constraints(table_name, column_name)
|
474
|
+
remove_default_constraint(table_name, column_name)
|
475
|
+
remove_indexes(table_name, column_name)
|
476
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
|
477
|
+
end
|
478
|
+
remove_sqlserver_columns_cache_for(table_name)
|
479
|
+
end
|
480
|
+
|
481
|
+
def change_column(table_name, column_name, type, options = {})
|
482
|
+
sql_commands = []
|
483
|
+
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
484
|
+
change_column_sql << " NOT NULL" if options[:null] == false
|
485
|
+
sql_commands << change_column_sql
|
486
|
+
if options_include_default?(options)
|
487
|
+
remove_default_constraint(table_name, column_name)
|
488
|
+
sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_name(table_name,column_name)} DEFAULT #{quote(options[:default])} FOR #{quote_column_name(column_name)}"
|
489
|
+
end
|
490
|
+
sql_commands.each { |c| do_execute(c) }
|
491
|
+
remove_sqlserver_columns_cache_for(table_name)
|
492
|
+
end
|
493
|
+
|
494
|
+
def change_column_default(table_name, column_name, default)
|
495
|
+
remove_default_constraint(table_name, column_name)
|
496
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_name(table_name, column_name)} DEFAULT #{quote(default)} FOR #{quote_column_name(column_name)}"
|
497
|
+
remove_sqlserver_columns_cache_for(table_name)
|
498
|
+
end
|
499
|
+
|
500
|
+
def rename_column(table_name, column_name, new_column_name)
|
501
|
+
column_for(table_name,column_name)
|
502
|
+
do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
|
503
|
+
remove_sqlserver_columns_cache_for(table_name)
|
504
|
+
end
|
505
|
+
|
506
|
+
def remove_index(table_name, options = {})
|
507
|
+
do_execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
|
508
|
+
end
|
509
|
+
|
510
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
511
|
+
limit = nil unless self.class.type_limitable?(type)
|
512
|
+
if type.to_s == 'integer'
|
513
|
+
case limit
|
514
|
+
when 1..2 then 'smallint'
|
515
|
+
when 3..4, nil then 'integer'
|
516
|
+
when 5..8 then 'bigint'
|
517
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
518
|
+
end
|
519
|
+
else
|
520
|
+
super
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def add_order_by_for_association_limiting!(sql, options)
|
525
|
+
# Disertation http://gist.github.com/24073
|
526
|
+
# Information http://weblogs.sqlteam.com/jeffs/archive/2007/12/13/select-distinct-order-by-error.aspx
|
527
|
+
return sql if options[:order].blank?
|
528
|
+
columns = sql.match(/SELECT\s+DISTINCT(.*)FROM/)[1].strip
|
529
|
+
sql.sub!(/SELECT\s+DISTINCT/,'SELECT')
|
530
|
+
sql << "GROUP BY #{columns} ORDER BY #{order_to_min_set(options[:order])}"
|
531
|
+
end
|
532
|
+
|
533
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
534
|
+
column = column_for(table_name,column_name)
|
535
|
+
unless null || default.nil?
|
536
|
+
do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
537
|
+
end
|
538
|
+
sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
|
539
|
+
sql << " NOT NULL" unless null
|
540
|
+
do_execute sql
|
541
|
+
end
|
542
|
+
|
543
|
+
def pk_and_sequence_for(table_name)
|
544
|
+
idcol = identity_column(table_name)
|
545
|
+
idcol ? [idcol.name,nil] : nil
|
546
|
+
end
|
547
|
+
|
548
|
+
# RAKE UTILITY METHODS =====================================#
|
549
|
+
|
550
|
+
def recreate_database(name)
|
551
|
+
existing_database = current_database.to_s
|
552
|
+
if name.to_s == existing_database
|
553
|
+
do_execute 'USE master'
|
554
|
+
end
|
555
|
+
drop_database(name)
|
556
|
+
create_database(name)
|
557
|
+
ensure
|
558
|
+
do_execute "USE #{existing_database}" if name.to_s == existing_database
|
559
|
+
end
|
560
|
+
|
561
|
+
def drop_database(name)
|
562
|
+
retry_count = 0
|
563
|
+
max_retries = 1
|
564
|
+
begin
|
565
|
+
do_execute "DROP DATABASE #{name}"
|
566
|
+
rescue ActiveRecord::StatementInvalid => err
|
567
|
+
# Remove existing connections and rollback any transactions if we received the message
|
568
|
+
# 'Cannot drop the database 'test' because it is currently in use'
|
569
|
+
if err.message =~ /because it is currently in use/
|
570
|
+
raise if retry_count >= max_retries
|
571
|
+
retry_count += 1
|
572
|
+
remove_database_connections_and_rollback(name)
|
573
|
+
retry
|
574
|
+
else
|
575
|
+
raise
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def create_database(name)
|
581
|
+
do_execute "CREATE DATABASE #{name}"
|
582
|
+
end
|
583
|
+
|
584
|
+
def current_database
|
585
|
+
select_value 'SELECT DB_NAME()'
|
586
|
+
end
|
587
|
+
|
588
|
+
def remove_database_connections_and_rollback(name)
|
589
|
+
# This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
|
590
|
+
# http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
|
591
|
+
do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
|
592
|
+
end
|
593
|
+
|
594
|
+
|
595
|
+
|
596
|
+
protected
|
597
|
+
|
598
|
+
# DATABASE STATEMENTS ======================================
|
599
|
+
|
600
|
+
def select(sql, name = nil, ignore_special_columns = false)
|
601
|
+
repair_special_columns(sql) unless ignore_special_columns
|
602
|
+
fields, rows = raw_select(sql,name)
|
603
|
+
rows.inject([]) do |results,row|
|
604
|
+
row_hash = {}
|
605
|
+
fields.each_with_index do |f, i|
|
606
|
+
row_hash[f] = row[i]
|
607
|
+
end
|
608
|
+
results << row_hash
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
613
|
+
set_utf8_values!(sql)
|
614
|
+
super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
|
615
|
+
end
|
616
|
+
|
617
|
+
def update_sql(sql, name = nil)
|
618
|
+
set_utf8_values!(sql)
|
619
|
+
execute(sql, name)
|
620
|
+
select_value('SELECT @@ROWCOUNT AS AffectedRows')
|
621
|
+
end
|
622
|
+
|
623
|
+
def raw_execute(sql, name = nil, &block)
|
624
|
+
log(sql, name) do
|
625
|
+
if block_given?
|
626
|
+
raw_connection.execute(sql) { |handle| yield(handle) }
|
627
|
+
else
|
628
|
+
raw_connection.execute(sql)
|
629
|
+
end
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def without_type_conversion
|
634
|
+
raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
|
635
|
+
yield
|
636
|
+
ensure
|
637
|
+
raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
|
638
|
+
end
|
639
|
+
|
640
|
+
def do_execute(sql,name=nil)
|
641
|
+
log(sql, name || 'EXECUTE') do
|
642
|
+
raw_connection.do(sql)
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
def raw_select(sql, name = nil)
|
647
|
+
handle = raw_execute(sql,name)
|
648
|
+
fields = handle.column_names
|
649
|
+
results = handle_as_array(handle)
|
650
|
+
rows = results.inject([]) do |rows,row|
|
651
|
+
row.each_with_index do |value, i|
|
652
|
+
# DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
|
653
|
+
if value.is_a? DBI::Timestamp
|
654
|
+
row[i] = value.to_sqlserver_string
|
655
|
+
end
|
656
|
+
end
|
657
|
+
rows << row
|
658
|
+
end
|
659
|
+
return fields, rows
|
660
|
+
end
|
661
|
+
|
662
|
+
def handle_as_array(handle)
|
663
|
+
array = handle.inject([]) do |rows,row|
|
664
|
+
rows << row.inject([]){ |values,value| values << value }
|
665
|
+
end
|
666
|
+
finish_statement_handle(handle)
|
667
|
+
array
|
668
|
+
end
|
669
|
+
|
670
|
+
def add_limit_offset_for_association_limiting!(sql, options)
|
671
|
+
sql.replace %|
|
672
|
+
SET NOCOUNT ON
|
673
|
+
DECLARE @row_number TABLE (row int identity(1,1), id int)
|
674
|
+
INSERT INTO @row_number (id)
|
675
|
+
#{sql}
|
676
|
+
SET NOCOUNT OFF
|
677
|
+
SELECT id FROM (
|
678
|
+
SELECT TOP #{options[:limit]} * FROM (
|
679
|
+
SELECT TOP #{options[:limit] + options[:offset]} * FROM @row_number ORDER BY row
|
680
|
+
) AS tmp1 ORDER BY row DESC
|
681
|
+
) AS tmp2 ORDER BY row
|
682
|
+
|.gsub(/[ \t\r\n]+/,' ')
|
683
|
+
end
|
684
|
+
|
685
|
+
# SCHEMA STATEMENTS ========================================#
|
686
|
+
|
687
|
+
def remove_check_constraints(table_name, column_name)
|
688
|
+
constraints = select_values("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'")
|
689
|
+
constraints.each do |constraint|
|
690
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
def remove_default_constraint(table_name, column_name)
|
695
|
+
constraints = select_values("SELECT def.name FROM sysobjects def, syscolumns col, sysobjects tab WHERE col.cdefault = def.id AND col.name = '#{quote_string(column_name)}' AND tab.name = '#{quote_string(table_name)}' AND col.id = tab.id")
|
696
|
+
constraints.each do |constraint|
|
697
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
def remove_indexes(table_name, column_name)
|
702
|
+
indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
|
703
|
+
remove_index(table_name, {:name => index.name})
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
def default_name(table_name, column_name)
|
708
|
+
"DF_#{table_name}_#{column_name}"
|
709
|
+
end
|
710
|
+
|
711
|
+
# IDENTITY INSERTS =========================================#
|
712
|
+
|
713
|
+
def with_identity_insert_enabled(table_name, &block)
|
714
|
+
set_identity_insert(table_name, true)
|
715
|
+
yield
|
716
|
+
ensure
|
717
|
+
set_identity_insert(table_name, false)
|
718
|
+
end
|
719
|
+
|
720
|
+
def set_identity_insert(table_name, enable = true)
|
721
|
+
sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
|
722
|
+
do_execute(sql,'IDENTITY_INSERT')
|
723
|
+
rescue Exception => e
|
724
|
+
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
|
725
|
+
end
|
726
|
+
|
727
|
+
def query_requires_identity_insert?(sql)
|
728
|
+
if insert_sql?(sql)
|
729
|
+
table_name = get_table_name(sql)
|
730
|
+
id_column = identity_column(table_name)
|
731
|
+
id_column && sql =~ /INSERT[^(]+\([^)]*\[#{id_column.name}\][^)]*\)/i ? table_name : false
|
732
|
+
else
|
733
|
+
false
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
def identity_column(table_name)
|
738
|
+
columns(table_name).detect(&:is_identity?)
|
739
|
+
end
|
740
|
+
|
741
|
+
# HELPER METHODS ===========================================#
|
742
|
+
|
743
|
+
def insert_sql?(sql)
|
744
|
+
!(sql =~ /^\s*INSERT/i).nil?
|
745
|
+
end
|
746
|
+
|
747
|
+
def unqualify_table_name(table_name)
|
748
|
+
table_name.to_s.split('.').last.gsub(/[\[\]]/,'')
|
749
|
+
end
|
750
|
+
|
751
|
+
def unqualify_db_name(table_name)
|
752
|
+
table_names = table_name.to_s.split('.')
|
753
|
+
table_names.length == 3 ? table_names.first.tr('[]','') : nil
|
754
|
+
end
|
755
|
+
|
756
|
+
def get_table_name(sql)
|
757
|
+
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
758
|
+
$1 || $2
|
759
|
+
elsif sql =~ /from\s+([^\(\s]+)\s*/i
|
760
|
+
$1
|
761
|
+
else
|
762
|
+
nil
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
def orders_and_dirs_set(order)
|
767
|
+
orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
|
768
|
+
orders_dirs = orders.map do |ord|
|
769
|
+
dir = nil
|
770
|
+
if match_data = ord.match(/\b(asc|desc)$/i)
|
771
|
+
dir = match_data[1]
|
772
|
+
ord.sub!(dir,'').strip!
|
773
|
+
dir.upcase!
|
774
|
+
end
|
775
|
+
[ord,dir]
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
def order_to_min_set(order)
|
780
|
+
orders_dirs = orders_and_dirs_set(order)
|
781
|
+
orders_dirs.map do |o,d|
|
782
|
+
"MIN(#{o}) #{d}".strip
|
783
|
+
end.join(', ')
|
784
|
+
end
|
785
|
+
|
786
|
+
def sql_for_association_limiting?(sql)
|
787
|
+
if md = sql.match(/^\s*SELECT(.*)FROM.*GROUP BY.*ORDER BY.*/im)
|
788
|
+
select_froms = md[1].split(',')
|
789
|
+
select_froms.size == 1 && !select_froms.first.include?('*')
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
def remove_sqlserver_columns_cache_for(table_name)
|
794
|
+
cache_key = unqualify_table_name(table_name)
|
795
|
+
@sqlserver_columns_cache[cache_key] = nil
|
796
|
+
end
|
797
|
+
|
798
|
+
def column_definitions(table_name)
|
799
|
+
db_name = unqualify_db_name(table_name)
|
800
|
+
table_name = unqualify_table_name(table_name)
|
801
|
+
# COL_LENGTH returns values that do not reflect how much data can be stored in certain data types.
|
802
|
+
# COL_LENGTH returns -1 for varchar(max), nvarchar(max), and varbinary(max)
|
803
|
+
# COL_LENGTH returns 16 for ntext, text, image types
|
804
|
+
sql = %{
|
805
|
+
SELECT
|
806
|
+
columns.COLUMN_NAME as name,
|
807
|
+
columns.DATA_TYPE as type,
|
808
|
+
CASE
|
809
|
+
WHEN columns.COLUMN_DEFAULT = '(null)' OR columns.COLUMN_DEFAULT = '(NULL)' THEN NULL
|
810
|
+
ELSE columns.COLUMN_DEFAULT
|
811
|
+
END as default_value,
|
812
|
+
columns.NUMERIC_SCALE as numeric_scale,
|
813
|
+
columns.NUMERIC_PRECISION as numeric_precision,
|
814
|
+
CASE
|
815
|
+
WHEN columns.DATA_TYPE IN ('nvarchar') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 1073741823
|
816
|
+
WHEN columns.DATA_TYPE IN ('varchar', 'varbinary') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 2147483647
|
817
|
+
WHEN columns.DATA_TYPE IN ('ntext') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 1073741823
|
818
|
+
WHEN columns.DATA_TYPE IN ('text', 'image') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 2147483647
|
819
|
+
ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
|
820
|
+
END as length,
|
821
|
+
CASE
|
822
|
+
WHEN columns.IS_NULLABLE = 'YES' THEN 1
|
823
|
+
ELSE NULL
|
824
|
+
end as is_nullable,
|
825
|
+
CASE
|
826
|
+
WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
|
827
|
+
ELSE 1
|
828
|
+
END as is_identity
|
829
|
+
FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
|
830
|
+
WHERE columns.TABLE_NAME = '#{table_name}'
|
831
|
+
ORDER BY columns.ordinal_position
|
832
|
+
}.gsub(/[ \t\r\n]+/,' ')
|
833
|
+
results = without_type_conversion { select(sql,nil,true) }
|
834
|
+
results.collect do |ci|
|
835
|
+
ci.symbolize_keys!
|
836
|
+
ci[:type] = if ci[:type] =~ /numeric|decimal/i
|
837
|
+
"#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
|
838
|
+
else
|
839
|
+
"#{ci[:type]}(#{ci[:length]})"
|
840
|
+
end
|
841
|
+
ci[:table_name] = table_name
|
842
|
+
ci[:default_value] = ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1] if ci[:default_value]
|
843
|
+
ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
|
844
|
+
ci
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
def column_for(table_name, column_name)
|
849
|
+
unless column = columns(table_name).detect { |c| c.name == column_name.to_s }
|
850
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
851
|
+
end
|
852
|
+
column
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
|
857
|
+
def change_order_direction(order)
|
858
|
+
order.split(",").collect {|fragment|
|
859
|
+
case fragment
|
860
|
+
when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
|
861
|
+
when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
|
862
|
+
else String.new(fragment).split(',').join(' DESC,') + ' DESC'
|
863
|
+
end
|
864
|
+
}.join(",")
|
865
|
+
end
|
866
|
+
|
867
|
+
def special_columns(table_name)
|
868
|
+
columns(table_name).select(&:is_special?).map(&:name)
|
869
|
+
end
|
870
|
+
|
871
|
+
def repair_special_columns(sql)
|
872
|
+
special_cols = special_columns(get_table_name(sql))
|
873
|
+
for col in special_cols.to_a
|
874
|
+
sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
|
875
|
+
sql.gsub!(/ORDER BY #{col.to_s}/i, '')
|
876
|
+
end
|
877
|
+
sql
|
878
|
+
end
|
879
|
+
|
880
|
+
def utf8_columns(table_name)
|
881
|
+
columns(table_name).select(&:is_utf8?).map(&:name)
|
882
|
+
end
|
883
|
+
|
884
|
+
def set_utf8_values!(sql)
|
885
|
+
utf8_cols = utf8_columns(get_table_name(sql))
|
886
|
+
if sql =~ /^\s*UPDATE/i
|
887
|
+
utf8_cols.each do |col|
|
888
|
+
sql.gsub!("[#{col.to_s}] = '", "[#{col.to_s}] = N'")
|
889
|
+
end
|
890
|
+
elsif sql =~ /^\s*INSERT(?!.*DEFAULT VALUES\s*$)/i
|
891
|
+
# TODO This code should be simplified
|
892
|
+
# Get columns and values, split them into arrays, and store the original_values for when we need to replace them
|
893
|
+
columns_and_values = sql.scan(/\((.*?)\)/m).flatten
|
894
|
+
columns = columns_and_values.first.split(',')
|
895
|
+
values = columns_and_values[1].split(',')
|
896
|
+
original_values = values.dup
|
897
|
+
# Iterate columns that should be UTF8, and append an N to the value, if the value is not NULL
|
898
|
+
utf8_cols.each do |col|
|
899
|
+
columns.each_with_index do |column, idx|
|
900
|
+
values[idx] = " N#{values[idx].gsub(/^ /, '')}" if column =~ /\[#{col}\]/ and values[idx] !~ /^NULL$/
|
901
|
+
end
|
902
|
+
end
|
903
|
+
# Replace (in place) the SQL
|
904
|
+
sql.gsub!(original_values.join(','), values.join(','))
|
905
|
+
end
|
906
|
+
end
|
907
|
+
|
908
|
+
end #class SQLServerAdapter < AbstractAdapter
|
909
|
+
|
910
|
+
end #module ConnectionAdapters
|
911
|
+
|
912
|
+
end #module ActiveRecord
|
913
|
+
|