ebryn-activerecord-sqlserver-adapter 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.
@@ -0,0 +1,979 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'bigdecimal/util'
|
6
|
+
|
7
|
+
# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
|
8
|
+
#
|
9
|
+
# Author: Joey Gibson <joey@joeygibson.com>
|
10
|
+
# Date: 10/14/2004
|
11
|
+
#
|
12
|
+
# Modifications: DeLynn Berry <delynnb@megastarfinancial.com>
|
13
|
+
# Date: 3/22/2005
|
14
|
+
#
|
15
|
+
# Modifications (ODBC): Mark Imbriaco <mark.imbriaco@pobox.com>
|
16
|
+
# Date: 6/26/2005
|
17
|
+
|
18
|
+
# Modifications (Migrations): Tom Ward <tom@popdog.net>
|
19
|
+
# Date: 27/10/2005
|
20
|
+
#
|
21
|
+
# Modifications (Numerous fixes as maintainer): Ryan Tomayko <rtomayko@gmail.com>
|
22
|
+
# Date: Up to July 2006
|
23
|
+
|
24
|
+
# Previous maintainer: Tom Ward <tom@popdog.net>
|
25
|
+
#
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
# Current (interim/unofficial) maintainer: Shawn Balestracci <shawn@vegantech.com>
|
31
|
+
|
32
|
+
module ActiveRecord
|
33
|
+
class Base
|
34
|
+
def self.sqlserver_connection(config) #:nodoc:
|
35
|
+
require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
|
36
|
+
|
37
|
+
config = config.symbolize_keys
|
38
|
+
|
39
|
+
mode = config[:mode] ? config[:mode].to_s.upcase : 'ADO'
|
40
|
+
username = config[:username] ? config[:username].to_s : 'sa'
|
41
|
+
password = config[:password] ? config[:password].to_s : ''
|
42
|
+
autocommit = config.key?(:autocommit) ? config[:autocommit] : true
|
43
|
+
if mode == "ODBC"
|
44
|
+
raise ArgumentError, "Missing DSN. Argument ':dsn' must be set in order for this adapter to work." unless config.has_key?(:dsn)
|
45
|
+
dsn = config[:dsn]
|
46
|
+
driver_url = "DBI:ODBC:#{dsn}"
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
|
49
|
+
database = config[:database]
|
50
|
+
host = config[:host] ? config[:host].to_s : 'localhost'
|
51
|
+
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
|
52
|
+
end
|
53
|
+
conn = DBI.connect(driver_url, username, password)
|
54
|
+
conn["AutoCommit"] = autocommit
|
55
|
+
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Add basic support for SQL server locking hints
|
62
|
+
# In the case of SQL server, the lock value must follow the FROM clause
|
63
|
+
# Mysql: SELECT * FROM tst where testID = 10 LOCK IN share mode
|
64
|
+
# SQLServer: SELECT * from tst WITH (HOLDLOCK, ROWLOCK) where testID = 10
|
65
|
+
# h-lame: OK, so these 2 methods should be a patch to rails ideally, so we don't
|
66
|
+
# have to play catch up against rails itself should construct_finder_sql ever
|
67
|
+
# change
|
68
|
+
def self.construct_finder_sql(options)
|
69
|
+
scope = scope(:find)
|
70
|
+
sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
|
71
|
+
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
|
72
|
+
|
73
|
+
add_lock!(sql, options, scope) if ActiveRecord::Base.connection.adapter_name == "SQLServer" && !options[:lock].blank? # SQLServer
|
74
|
+
|
75
|
+
# merge_joins isn't defined in 2.1.1, but appears in edge
|
76
|
+
if defined?(merge_joins)
|
77
|
+
# The next line may fail with a nil error under 2.1.1 or other non-edge rails versions - Use this instead: add_joins!(sql, options, scope)
|
78
|
+
add_joins!(sql, options[:joins], scope)
|
79
|
+
else
|
80
|
+
add_joins!(sql, options, scope)
|
81
|
+
end
|
82
|
+
|
83
|
+
add_conditions!(sql, options[:conditions], scope)
|
84
|
+
|
85
|
+
add_group!(sql, options[:group], scope)
|
86
|
+
add_order!(sql, options[:order], scope)
|
87
|
+
add_limit!(sql, options, scope)
|
88
|
+
add_lock!(sql, options, scope) unless ActiveRecord::Base.connection.adapter_name == "SQLServer" # Not SQLServer
|
89
|
+
sql
|
90
|
+
end
|
91
|
+
|
92
|
+
# Overwrite the ActiveRecord::Base method for SQL server.
|
93
|
+
# GROUP BY is necessary for distinct orderings
|
94
|
+
def self.construct_finder_sql_for_association_limiting(options, join_dependency)
|
95
|
+
scope = scope(:find)
|
96
|
+
is_distinct = !options[:joins].blank? || include_eager_conditions?(options) || include_eager_order?(options)
|
97
|
+
|
98
|
+
sql = "SELECT #{table_name}.#{connection.quote_column_name(primary_key)} FROM #{table_name} "
|
99
|
+
|
100
|
+
if is_distinct
|
101
|
+
sql << join_dependency.join_associations.collect(&:association_join).join
|
102
|
+
# merge_joins isn't defined in 2.1.1, but appears in edge
|
103
|
+
if defined?(merge_joins)
|
104
|
+
# The next line may fail with a nil error under 2.1.1 or other non-edge rails versions - Use this instead: add_joins!(sql, options, scope)
|
105
|
+
add_joins!(sql, options[:joins], scope)
|
106
|
+
else
|
107
|
+
add_joins!(sql, options, scope)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
add_conditions!(sql, options[:conditions], scope)
|
112
|
+
add_group!(sql, options[:group], scope)
|
113
|
+
|
114
|
+
if options[:order] && is_distinct
|
115
|
+
if sql =~ /GROUP\s+BY/i
|
116
|
+
sql << ", #{table_name}.#{connection.quote_column_name(primary_key)}"
|
117
|
+
else
|
118
|
+
sql << " GROUP BY #{table_name}.#{connection.quote_column_name(primary_key)}"
|
119
|
+
end #if sql =~ /GROUP BY/i
|
120
|
+
|
121
|
+
connection.add_order_by_for_association_limiting!(sql, options)
|
122
|
+
else
|
123
|
+
add_order!(sql, options[:order], scope)
|
124
|
+
end
|
125
|
+
|
126
|
+
add_limit!(sql, options, scope)
|
127
|
+
|
128
|
+
return sanitize_sql(sql)
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
end # class Base
|
133
|
+
|
134
|
+
module ConnectionAdapters
|
135
|
+
class SQLServerColumn < Column# :nodoc:
|
136
|
+
attr_reader :identity, :is_special, :is_utf8
|
137
|
+
|
138
|
+
def initialize(info)
|
139
|
+
if info[:type] =~ /numeric|decimal/i
|
140
|
+
type = "#{info[:type]}(#{info[:numeric_precision]},#{info[:numeric_scale]})"
|
141
|
+
else
|
142
|
+
type = "#{info[:type]}(#{info[:length]})"
|
143
|
+
end
|
144
|
+
super(info[:name], info[:default_value], type, info[:is_nullable] == 1)
|
145
|
+
@identity = info[:is_identity]
|
146
|
+
|
147
|
+
# TODO: Not sure if these should also be special: varbinary(max), nchar, nvarchar(max)
|
148
|
+
@is_special = ["text", "ntext", "image"].include?(info[:type])
|
149
|
+
|
150
|
+
# Added nchar and nvarchar(max) for unicode types
|
151
|
+
# http://www.teratrax.com/sql_guide/data_types/sql_server_data_types.html
|
152
|
+
@is_utf8 = type =~ /nvarchar|ntext|nchar|nvarchar(max)/i
|
153
|
+
# TODO: check ok to remove @scale = scale_value
|
154
|
+
@limit = nil unless limitable?(type)
|
155
|
+
end
|
156
|
+
|
157
|
+
def limitable?(type)
|
158
|
+
# SQL Server only supports limits on *char and float types
|
159
|
+
# although for schema dumping purposes it's useful to know that (big|small)int are 2|8 respectively.
|
160
|
+
@type == :float || @type == :string || (@type == :integer && type =~ /^(big|small)int/)
|
161
|
+
end
|
162
|
+
|
163
|
+
def simplified_type(field_type)
|
164
|
+
case field_type
|
165
|
+
when /real/i then :float
|
166
|
+
when /money/i then :decimal
|
167
|
+
when /image/i then :binary
|
168
|
+
when /bit/i then :boolean
|
169
|
+
when /uniqueidentifier/i then :string
|
170
|
+
else super
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def type_cast(value)
|
175
|
+
return nil if value.nil?
|
176
|
+
case type
|
177
|
+
when :datetime then self.class.cast_to_datetime(value)
|
178
|
+
when :timestamp then self.class.cast_to_time(value)
|
179
|
+
when :time then self.class.cast_to_time(value)
|
180
|
+
when :date then self.class.cast_to_datetime(value)
|
181
|
+
else super
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def type_cast_code(var_name)
|
186
|
+
case type
|
187
|
+
when :datetime then "#{self.class.name}.cast_to_datetime(#{var_name})"
|
188
|
+
when :timestamp then "#{self.class.name}.cast_to_time(#{var_name})"
|
189
|
+
when :time then "#{self.class.name}.cast_to_time(#{var_name})"
|
190
|
+
when :date then "#{self.class.name}.cast_to_datetime(#{var_name})"
|
191
|
+
else super
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class << self
|
196
|
+
def cast_to_datetime(value)
|
197
|
+
return value.to_time if value.is_a?(DBI::Timestamp)
|
198
|
+
return string_to_time(value) if value.is_a?(Time)
|
199
|
+
return string_to_time(value) if value.is_a?(DateTime)
|
200
|
+
return cast_to_time(value) if value.is_a?(String)
|
201
|
+
value
|
202
|
+
end
|
203
|
+
|
204
|
+
def cast_to_time(value)
|
205
|
+
return value if value.is_a?(Time)
|
206
|
+
time_hash = Date._parse(value)
|
207
|
+
time_hash[:sec_fraction] = 0 # REVISIT: microseconds(time_hash)
|
208
|
+
new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) rescue nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def string_to_time(value)
|
212
|
+
if value.is_a?(DateTime) || value.is_a?(Time)
|
213
|
+
# The DateTime comes in as '2008-08-08T17:57:28+00:00'
|
214
|
+
# Original code was taking a UTC DateTime, ignored the time zone by
|
215
|
+
# creating a localized Time object, ex: 'FRI Aug 08 17:57:28 +04 2008'
|
216
|
+
# Instead, let Time.parse translate the DateTime string including it's timezone
|
217
|
+
# If Rails is UTC, call .utc, otherwise return a local time value
|
218
|
+
return Base.default_timezone == :utc ? Time.parse(value.to_s).utc : Time.parse(value.to_s)
|
219
|
+
else
|
220
|
+
super
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# To insert into a SQL server binary column, the value must be
|
225
|
+
# converted to hex characters and prepended with 0x
|
226
|
+
# Example: INSERT into varbinarytable values (0x0)
|
227
|
+
# See the output of the stored procedure: 'exec sp_datatype_info'
|
228
|
+
# and note the literal prefix value of 0x for binary types
|
229
|
+
def string_to_binary(value)
|
230
|
+
"0x#{value.unpack("H*")[0]}"
|
231
|
+
end
|
232
|
+
|
233
|
+
def binary_to_string(value)
|
234
|
+
# Check if the value actually is hex output from the database
|
235
|
+
# or an Active Record attribute that was just written. If hex, pack the hex
|
236
|
+
# characters into a string, otherwise return the value
|
237
|
+
# TODO: This conversion is asymmetrical, and could corrupt data if the original data looked like hex. We need to avoid the guesswork
|
238
|
+
value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
|
239
|
+
end
|
240
|
+
|
241
|
+
protected
|
242
|
+
def new_time(year, mon, mday, hour, min, sec, microsec = 0)
|
243
|
+
# Treat 0000-00-00 00:00:00 as nil.
|
244
|
+
return nil if year.nil? || year == 0
|
245
|
+
Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
246
|
+
end
|
247
|
+
end #class << self
|
248
|
+
end #SQLServerColumn
|
249
|
+
|
250
|
+
# In ADO mode, this adapter will ONLY work on Windows systems,
|
251
|
+
# since it relies on Win32OLE, which, to my knowledge, is only
|
252
|
+
# available on Windows.
|
253
|
+
#
|
254
|
+
# This mode also relies on the ADO support in the DBI module. If you are using the
|
255
|
+
# one-click installer of Ruby, then you already have DBI installed, but
|
256
|
+
# the ADO module is *NOT* installed. You will need to get the latest
|
257
|
+
# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
|
258
|
+
# unzip it, and copy the file
|
259
|
+
# <tt>src/lib/dbd_ado/ADO.rb</tt>
|
260
|
+
# to
|
261
|
+
# <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
|
262
|
+
# (you will more than likely need to create the ADO directory).
|
263
|
+
# Once you've installed that file, you are ready to go.
|
264
|
+
#
|
265
|
+
# In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
|
266
|
+
# the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
|
267
|
+
# and it is available at http://www.ch-werner.de/rubyodbc/
|
268
|
+
#
|
269
|
+
# Options:
|
270
|
+
#
|
271
|
+
# * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
|
272
|
+
# * <tt>:username</tt> -- Defaults to sa.
|
273
|
+
# * <tt>:password</tt> -- Defaults to empty string.
|
274
|
+
# * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
|
275
|
+
#
|
276
|
+
# ADO specific options:
|
277
|
+
#
|
278
|
+
# * <tt>:host</tt> -- Defaults to localhost.
|
279
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
280
|
+
# * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
|
281
|
+
#
|
282
|
+
# ODBC specific options:
|
283
|
+
#
|
284
|
+
# * <tt>:dsn</tt> -- Defaults to nothing.
|
285
|
+
#
|
286
|
+
# ADO code tested on Windows 2000 and higher systems,
|
287
|
+
# running ruby 1.8.2 (2004-07-29) [i386-mswin32], and SQL Server 2000 SP3.
|
288
|
+
#
|
289
|
+
# ODBC code tested on a Fedora Core 4 system, running FreeTDS 0.63,
|
290
|
+
# unixODBC 2.2.11, Ruby ODBC 0.996, Ruby DBI 0.0.23 and Ruby 1.8.2.
|
291
|
+
# [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux]
|
292
|
+
class SQLServerAdapter < AbstractAdapter
|
293
|
+
|
294
|
+
def initialize(connection, logger, connection_options=nil)
|
295
|
+
super(connection, logger)
|
296
|
+
@connection_options = connection_options
|
297
|
+
if database_version =~ /(2000|2005) - (\d+)\./
|
298
|
+
@database_version_year = $1.to_i
|
299
|
+
@database_version_major = $2.to_i
|
300
|
+
else
|
301
|
+
raise "Currently, only 2000 and 2005 are supported versions"
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
def native_database_types
|
307
|
+
# support for varchar(max) and varbinary(max) for text and binary cols if our version is 9 (2005)
|
308
|
+
txt = @database_version_major >= 9 ? "varchar(max)" : "text"
|
309
|
+
|
310
|
+
# TODO: Need to verify image column works correctly with 2000 if string_to_binary stores a hex string
|
311
|
+
bin = @database_version_major >= 9 ? "varbinary(max)" : "image"
|
312
|
+
{
|
313
|
+
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
|
314
|
+
:string => { :name => "varchar", :limit => 255 },
|
315
|
+
:text => { :name => txt },
|
316
|
+
:integer => { :name => "int" },
|
317
|
+
:float => { :name => "float", :limit => 8 },
|
318
|
+
:decimal => { :name => "decimal" },
|
319
|
+
:datetime => { :name => "datetime" },
|
320
|
+
:timestamp => { :name => "datetime" },
|
321
|
+
:time => { :name => "datetime" },
|
322
|
+
:date => { :name => "datetime" },
|
323
|
+
:binary => { :name => bin },
|
324
|
+
:boolean => { :name => "bit"}
|
325
|
+
}
|
326
|
+
end
|
327
|
+
|
328
|
+
def adapter_name
|
329
|
+
'SQLServer'
|
330
|
+
end
|
331
|
+
|
332
|
+
def database_version
|
333
|
+
# returns string such as:
|
334
|
+
# "Microsoft SQL Server 2000 - 8.00.2039 (Intel X86) \n\tMay 3 2005 23:18:38 \n\tCopyright (c) 1988-2003 Microsoft Corporation\n\tEnterprise Edition on Windows NT 5.2 (Build 3790: )\n"
|
335
|
+
# "Microsoft SQL Server 2005 - 9.00.3215.00 (Intel X86) \n\tDec 8 2007 18:51:32 \n\tCopyright (c) 1988-2005 Microsoft Corporation\n\tStandard Edition on Windows NT 5.2 (Build 3790: Service Pack 2)\n"
|
336
|
+
return select_value("SELECT @@version")
|
337
|
+
end
|
338
|
+
|
339
|
+
def supports_migrations? #:nodoc:
|
340
|
+
true
|
341
|
+
end
|
342
|
+
|
343
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
344
|
+
# Remove limit for data types which do not require it
|
345
|
+
# Valid: ALTER TABLE sessions ALTER COLUMN [data] varchar(max)
|
346
|
+
# Invalid: ALTER TABLE sessions ALTER COLUMN [data] varchar(max)(16777215)
|
347
|
+
limit = nil if %w{text varchar(max) nvarchar(max) ntext varbinary(max) image}.include?(native_database_types[type.to_sym][:name])
|
348
|
+
|
349
|
+
return super unless type.to_s == 'integer'
|
350
|
+
|
351
|
+
if limit.nil?
|
352
|
+
'integer'
|
353
|
+
elsif limit > 4
|
354
|
+
'bigint'
|
355
|
+
elsif limit < 3
|
356
|
+
'smallint'
|
357
|
+
else
|
358
|
+
'integer'
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# CONNECTION MANAGEMENT ====================================#
|
363
|
+
|
364
|
+
# Returns true if the connection is active.
|
365
|
+
def active?
|
366
|
+
@connection.execute("SELECT 1").finish
|
367
|
+
true
|
368
|
+
rescue DBI::DatabaseError, DBI::InterfaceError
|
369
|
+
false
|
370
|
+
end
|
371
|
+
|
372
|
+
# Reconnects to the database, returns false if no connection could be made.
|
373
|
+
def reconnect!
|
374
|
+
disconnect!
|
375
|
+
@connection = DBI.connect(*@connection_options)
|
376
|
+
rescue DBI::DatabaseError => e
|
377
|
+
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
|
378
|
+
false
|
379
|
+
end
|
380
|
+
|
381
|
+
# Disconnects from the database
|
382
|
+
|
383
|
+
def disconnect!
|
384
|
+
@connection.disconnect rescue nil
|
385
|
+
end
|
386
|
+
|
387
|
+
def select_rows(sql, name = nil)
|
388
|
+
rows = []
|
389
|
+
repair_special_columns(sql)
|
390
|
+
log(sql, name) do
|
391
|
+
@connection.select_all(sql) do |row|
|
392
|
+
record = []
|
393
|
+
row.each do |col|
|
394
|
+
if col.is_a? DBI::Timestamp
|
395
|
+
record << col.to_time
|
396
|
+
else
|
397
|
+
record << col
|
398
|
+
end
|
399
|
+
end
|
400
|
+
rows << record
|
401
|
+
end
|
402
|
+
end
|
403
|
+
rows
|
404
|
+
end
|
405
|
+
|
406
|
+
def columns(table_name, name = nil)
|
407
|
+
return [] if table_name.blank?
|
408
|
+
table_names = table_name.to_s.split('.')
|
409
|
+
table_name = table_names[-1]
|
410
|
+
table_name = table_name.gsub(/[\[\]]/, '')
|
411
|
+
db_name = "#{table_names[0]}." if table_names.length==3
|
412
|
+
|
413
|
+
# COL_LENGTH returns values that do not reflect how much data can be stored in certain data types.
|
414
|
+
# COL_LENGTH returns -1 for varchar(max), nvarchar(max), and varbinary(max)
|
415
|
+
# COL_LENGTH returns 16 for ntext, text, image types
|
416
|
+
# My sessions.data column was varchar(max) and resulted in the following error:
|
417
|
+
# Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.
|
418
|
+
sql = %{
|
419
|
+
SELECT
|
420
|
+
columns.COLUMN_NAME as name,
|
421
|
+
columns.DATA_TYPE as type,
|
422
|
+
CASE
|
423
|
+
WHEN columns.COLUMN_DEFAULT = '(null)' OR columns.COLUMN_DEFAULT = '(NULL)' THEN NULL
|
424
|
+
ELSE columns.COLUMN_DEFAULT
|
425
|
+
END default_value,
|
426
|
+
columns.NUMERIC_SCALE as numeric_scale,
|
427
|
+
columns.NUMERIC_PRECISION as numeric_precision,
|
428
|
+
CASE
|
429
|
+
WHEN columns.DATA_TYPE IN ('nvarchar') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 1073741823
|
430
|
+
WHEN columns.DATA_TYPE IN ('varchar', 'varbinary') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 2147483647
|
431
|
+
WHEN columns.DATA_TYPE IN ('ntext') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 1073741823
|
432
|
+
WHEN columns.DATA_TYPE IN ('text', 'image') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 2147483647
|
433
|
+
ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
|
434
|
+
END as length,
|
435
|
+
CASE
|
436
|
+
WHEN columns.IS_NULLABLE = 'YES' THEN 1
|
437
|
+
ELSE NULL
|
438
|
+
end is_nullable,
|
439
|
+
CASE
|
440
|
+
WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
|
441
|
+
ELSE 1
|
442
|
+
END is_identity
|
443
|
+
FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
|
444
|
+
WHERE columns.TABLE_NAME = '#{table_name}'
|
445
|
+
ORDER BY columns.ordinal_position
|
446
|
+
}.gsub(/[ \t\r\n]+/,' ')
|
447
|
+
result = select(sql, name, true)
|
448
|
+
result.collect do |column_info|
|
449
|
+
# Remove brackets and outer quotes (if quoted) of default value returned by db, i.e:
|
450
|
+
# "(1)" => "1", "('1')" => "1", "((-1))" => "-1", "('(-1)')" => "(-1)"
|
451
|
+
# Unicode strings will be prefixed with an N. Remove that too.
|
452
|
+
column_info.symbolize_keys!
|
453
|
+
column_info[:default_value] = column_info[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1] if column_info[:default_value]
|
454
|
+
SQLServerColumn.new(column_info)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
def empty_insert_statement(table_name)
|
459
|
+
"INSERT INTO #{table_name} DEFAULT VALUES"
|
460
|
+
end
|
461
|
+
|
462
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
463
|
+
set_utf8_values!(sql)
|
464
|
+
super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
|
465
|
+
end
|
466
|
+
|
467
|
+
def update_sql(sql, name = nil)
|
468
|
+
set_utf8_values!(sql)
|
469
|
+
auto_commiting = @connection["AutoCommit"]
|
470
|
+
begin
|
471
|
+
begin_db_transaction if auto_commiting
|
472
|
+
execute(sql, name)
|
473
|
+
affected_rows = select_value("SELECT @@ROWCOUNT AS AffectedRows")
|
474
|
+
commit_db_transaction if auto_commiting
|
475
|
+
affected_rows
|
476
|
+
rescue
|
477
|
+
rollback_db_transaction if auto_commiting
|
478
|
+
raise
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
def execute(sql, name = nil)
|
483
|
+
if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql))
|
484
|
+
log(sql, name) do
|
485
|
+
with_identity_insert_enabled(table_name) do
|
486
|
+
@connection.execute(sql) do |handle|
|
487
|
+
yield(handle) if block_given?
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
else
|
492
|
+
log(sql, name) do
|
493
|
+
@connection.execute(sql) do |handle|
|
494
|
+
yield(handle) if block_given?
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def begin_db_transaction
|
501
|
+
@connection["AutoCommit"] = false
|
502
|
+
rescue Exception => e
|
503
|
+
@connection["AutoCommit"] = true
|
504
|
+
end
|
505
|
+
|
506
|
+
def commit_db_transaction
|
507
|
+
@connection.commit
|
508
|
+
ensure
|
509
|
+
@connection["AutoCommit"] = true
|
510
|
+
end
|
511
|
+
|
512
|
+
def rollback_db_transaction
|
513
|
+
@connection.rollback
|
514
|
+
ensure
|
515
|
+
@connection["AutoCommit"] = true
|
516
|
+
end
|
517
|
+
|
518
|
+
def quote(value, column = nil)
|
519
|
+
return value.quoted_id if value.respond_to?(:quoted_id)
|
520
|
+
|
521
|
+
case value
|
522
|
+
when TrueClass then '1'
|
523
|
+
when FalseClass then '0'
|
524
|
+
|
525
|
+
when String, ActiveSupport::Multibyte::Chars
|
526
|
+
value = value.to_s
|
527
|
+
|
528
|
+
# for binary columns, don't quote the result of the string to binary
|
529
|
+
return column.class.string_to_binary(value) if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
530
|
+
super
|
531
|
+
else
|
532
|
+
if value.acts_like?(:time)
|
533
|
+
"'#{value.strftime("%Y%m%d %H:%M:%S")}'"
|
534
|
+
elsif value.acts_like?(:date)
|
535
|
+
"'#{value.strftime("%Y%m%d")}'"
|
536
|
+
else
|
537
|
+
super
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
def quote_string(string)
|
543
|
+
string.gsub(/\'/, "''")
|
544
|
+
end
|
545
|
+
|
546
|
+
def quote_table_name(name)
|
547
|
+
name_split_on_dots = name.to_s.split('.')
|
548
|
+
|
549
|
+
if name_split_on_dots.length == 3
|
550
|
+
# name is on the form "foo.bar.baz"
|
551
|
+
"[#{name_split_on_dots[0]}].[#{name_split_on_dots[1]}].[#{name_split_on_dots[2]}]"
|
552
|
+
else
|
553
|
+
super(name)
|
554
|
+
end
|
555
|
+
|
556
|
+
end
|
557
|
+
|
558
|
+
# Quotes the given column identifier.
|
559
|
+
#
|
560
|
+
# Examples
|
561
|
+
#
|
562
|
+
# quote_column_name('foo') # => '[foo]'
|
563
|
+
# quote_column_name(:foo) # => '[foo]'
|
564
|
+
# quote_column_name('foo.bar') # => '[foo].[bar]'
|
565
|
+
def quote_column_name(identifier)
|
566
|
+
identifier.to_s.split('.').collect do |name|
|
567
|
+
"[#{name}]"
|
568
|
+
end.join(".")
|
569
|
+
end
|
570
|
+
|
571
|
+
def add_limit_offset!(sql, options)
|
572
|
+
if options[:offset]
|
573
|
+
raise ArgumentError, "offset should have a limit" unless options[:limit]
|
574
|
+
unless options[:offset].kind_of?Integer
|
575
|
+
if options[:offset] =~ /^\d+$/
|
576
|
+
options[:offset] = options[:offset].to_i
|
577
|
+
else
|
578
|
+
raise ArgumentError, "offset should be an integer"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
if options[:limit] && !(options[:limit].kind_of?Integer)
|
584
|
+
# is it just a string which should be an integer?
|
585
|
+
if options[:limit] =~ /^\d+$/
|
586
|
+
options[:limit] = options[:limit].to_i
|
587
|
+
else
|
588
|
+
raise ArgumentError, "limit should be an integer"
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
if options[:limit] and options[:offset]
|
593
|
+
total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally")[0][:TotalRows].to_i
|
594
|
+
if (options[:limit] + options[:offset]) >= total_rows
|
595
|
+
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
|
596
|
+
end
|
597
|
+
|
598
|
+
# Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
|
599
|
+
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
|
600
|
+
sql << ") AS tmp1"
|
601
|
+
|
602
|
+
if options[:order]
|
603
|
+
order = options[:order].split(',').map do |field|
|
604
|
+
order_by_column, order_direction = field.split(" ")
|
605
|
+
order_by_column = quote_column_name(order_by_column)
|
606
|
+
|
607
|
+
# Investigate the SQL query to figure out if the order_by_column has been renamed.
|
608
|
+
if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
|
609
|
+
# Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
|
610
|
+
order_by_column = $1
|
611
|
+
elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
|
612
|
+
order_by_column = $1
|
613
|
+
else
|
614
|
+
# It doesn't appear that the column name has been renamed as part of the query. Use just the column
|
615
|
+
# name rather than the full identifier for the outer queries.
|
616
|
+
order_by_column = order_by_column.split('.').last
|
617
|
+
end
|
618
|
+
|
619
|
+
# Put the column name and eventual direction back together
|
620
|
+
[order_by_column, order_direction].join(' ').strip
|
621
|
+
end.join(', ')
|
622
|
+
|
623
|
+
sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
|
624
|
+
else
|
625
|
+
sql << ") AS tmp2"
|
626
|
+
end
|
627
|
+
elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
|
628
|
+
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do
|
629
|
+
"SELECT#{$1} TOP #{options[:limit]}"
|
630
|
+
end unless options[:limit].nil? || options[:limit] < 1
|
631
|
+
end
|
632
|
+
end #add_limit_offset!(sql, options)
|
633
|
+
|
634
|
+
def add_order_by_for_association_limiting!(sql, options)
|
635
|
+
return sql if options[:order].blank?
|
636
|
+
|
637
|
+
# Strip any ASC or DESC from the orders for the select list
|
638
|
+
# Build fields and order arrays
|
639
|
+
# e.g.: options[:order] = 'table.[id], table2.[col2] desc'
|
640
|
+
# fields = ['min(table.[id]) AS id', 'min(table2.[col2]) AS col2']
|
641
|
+
# order = ['id', 'col2 desc']
|
642
|
+
fields = []
|
643
|
+
order = []
|
644
|
+
options[:order].split(/\s*,\s*/).each do |str|
|
645
|
+
# regex matches 'table_name.[column_name] asc' or 'column_name' ('table_name.', 'asc', '[', and ']' are optional)
|
646
|
+
# $1 = 'table_name.[column_name]'
|
647
|
+
# $2 = 'column_name'
|
648
|
+
# $3 = ' asc'
|
649
|
+
str =~ /((?:\w+\.)?\[?(\w+)\]?)(\s+asc|\s+desc)?/i
|
650
|
+
fields << "MIN(#{$1}) AS #{$2}"
|
651
|
+
order << "#{$2}#{$3}"
|
652
|
+
end
|
653
|
+
|
654
|
+
sql.gsub!(/(.+?) FROM/, "\\1, #{fields.join(',')} FROM")
|
655
|
+
sql << " ORDER BY #{order.join(',')}"
|
656
|
+
end
|
657
|
+
|
658
|
+
# Appends a locking clause to an SQL statement.
|
659
|
+
# This method *modifies* the +sql+ parameter.
|
660
|
+
# # SELECT * FROM suppliers FOR UPDATE
|
661
|
+
# add_lock! 'SELECT * FROM suppliers', :lock => true
|
662
|
+
# add_lock! 'SELECT * FROM suppliers', :lock => ' WITH(HOLDLOCK, ROWLOCK)'
|
663
|
+
# http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
|
664
|
+
def add_lock!(sql, options)
|
665
|
+
case lock = options[:lock]
|
666
|
+
when true then sql << "WITH(HOLDLOCK, ROWLOCK) "
|
667
|
+
when String then sql << "#{lock} "
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def recreate_database(name)
|
672
|
+
# Switch to another database or we'll receive a "Database in use" error message.
|
673
|
+
existing_database = current_database.to_s
|
674
|
+
if name.to_s == existing_database
|
675
|
+
# The master database should be available on all SQL Server instances, use that
|
676
|
+
execute 'USE master'
|
677
|
+
end
|
678
|
+
|
679
|
+
# Recreate the database
|
680
|
+
drop_database(name)
|
681
|
+
create_database(name)
|
682
|
+
|
683
|
+
# Switch back to the database if we switched away from it above
|
684
|
+
execute "USE #{existing_database}" if name.to_s == existing_database
|
685
|
+
end
|
686
|
+
|
687
|
+
def remove_database_connections_and_rollback(name)
|
688
|
+
# This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
|
689
|
+
# http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
|
690
|
+
execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
|
691
|
+
end
|
692
|
+
|
693
|
+
def drop_database(name)
|
694
|
+
retry_count = 0
|
695
|
+
max_retries = 1
|
696
|
+
begin
|
697
|
+
execute "DROP DATABASE #{name}"
|
698
|
+
rescue ActiveRecord::StatementInvalid => err
|
699
|
+
# Remove existing connections and rollback any transactions if we received the message
|
700
|
+
# 'Cannot drop the database 'test' because it is currently in use'
|
701
|
+
if err.message =~ /because it is currently in use/
|
702
|
+
raise if retry_count >= max_retries
|
703
|
+
retry_count += 1
|
704
|
+
remove_database_connections_and_rollback(name)
|
705
|
+
retry
|
706
|
+
else
|
707
|
+
raise
|
708
|
+
end
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
# Clear the given table and reset the table's id to 1
|
713
|
+
# Argument:
|
714
|
+
# +table_name+:: (String) Name of the table to be cleared and reset
|
715
|
+
def truncate(table_name)
|
716
|
+
execute("TRUNCATE TABLE #{table_name}; DBCC CHECKIDENT ('#{table_name}', RESEED, 1)")
|
717
|
+
end #truncate
|
718
|
+
|
719
|
+
def create_database(name)
|
720
|
+
execute "CREATE DATABASE #{name}"
|
721
|
+
end
|
722
|
+
|
723
|
+
def current_database
|
724
|
+
@connection.select_one("SELECT DB_NAME()")[0]
|
725
|
+
end
|
726
|
+
|
727
|
+
def tables(name = nil)
|
728
|
+
execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", name) do |sth|
|
729
|
+
result = sth.inject([]) do |tables, field|
|
730
|
+
table_name = field[0]
|
731
|
+
tables << table_name unless table_name == 'dtproperties'
|
732
|
+
tables
|
733
|
+
end
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
def table_exists?(table_name)
|
738
|
+
#If the table is external, see if it has columns
|
739
|
+
super(table_name) || (columns(table_name).size>0)
|
740
|
+
end
|
741
|
+
|
742
|
+
def indexes(table_name, name = nil)
|
743
|
+
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = false
|
744
|
+
__indexes(table_name, name)
|
745
|
+
ensure
|
746
|
+
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
|
747
|
+
end
|
748
|
+
|
749
|
+
def rename_table(name, new_name)
|
750
|
+
execute "EXEC sp_rename '#{name}', '#{new_name}'"
|
751
|
+
end
|
752
|
+
|
753
|
+
def add_column(table_name, column_name, type, options = {})
|
754
|
+
add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
755
|
+
add_column_options!(add_column_sql, options)
|
756
|
+
# TODO: Add support to mimic date columns, using constraints to mark them as such in the database
|
757
|
+
# add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date
|
758
|
+
execute(add_column_sql)
|
759
|
+
end
|
760
|
+
|
761
|
+
def rename_column(table_name, column_name, new_column_name)
|
762
|
+
if columns(table_name).find{|c| c.name.to_s == column_name.to_s}
|
763
|
+
execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}'"
|
764
|
+
else
|
765
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
770
|
+
sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
771
|
+
sql << " NOT NULL" if options[:null] == false
|
772
|
+
sql_commands = [sql]
|
773
|
+
if options_include_default?(options)
|
774
|
+
remove_default_constraint(table_name, column_name)
|
775
|
+
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{quote_column_name(column_name)}"
|
776
|
+
end
|
777
|
+
sql_commands.each {|c|
|
778
|
+
execute(c)
|
779
|
+
}
|
780
|
+
end
|
781
|
+
|
782
|
+
def change_column_default(table_name, column_name, default)
|
783
|
+
remove_default_constraint(table_name, column_name)
|
784
|
+
execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default, column_name)} FOR #{quote_column_name(column_name)}"
|
785
|
+
end
|
786
|
+
|
787
|
+
def remove_column(table_name, column_name)
|
788
|
+
remove_check_constraints(table_name, column_name)
|
789
|
+
remove_default_constraint(table_name, column_name)
|
790
|
+
remove_indexes(table_name, column_name)
|
791
|
+
execute "ALTER TABLE [#{table_name}] DROP COLUMN #{quote_column_name(column_name)}"
|
792
|
+
end
|
793
|
+
|
794
|
+
def remove_default_constraint(table_name, column_name)
|
795
|
+
constraints = select "SELECT def.name FROM sysobjects def, syscolumns col, sysobjects tab WHERE col.cdefault = def.id AND col.name = '#{column_name}' AND tab.name = '#{table_name}' AND col.id = tab.id"
|
796
|
+
|
797
|
+
constraints.each do |constraint|
|
798
|
+
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}"
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
def remove_check_constraints(table_name, column_name)
|
803
|
+
# TODO remove all constraints in single method
|
804
|
+
constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'"
|
805
|
+
constraints.each do |constraint|
|
806
|
+
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}"
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
def remove_indexes(table_name, column_name)
|
811
|
+
__indexes(table_name).select {|idx| idx.columns.include? column_name }.each do |idx|
|
812
|
+
remove_index(table_name, {:name => idx.name})
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
def remove_index(table_name, options = {})
|
817
|
+
execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
|
818
|
+
end
|
819
|
+
|
820
|
+
# Returns a table's primary key and belonging sequence (not applicable to SQL server).
|
821
|
+
def pk_and_sequence_for(table_name)
|
822
|
+
@connection["AutoCommit"] = false
|
823
|
+
keys = []
|
824
|
+
execute("EXEC sp_helpindex '#{table_name}'") do |handle|
|
825
|
+
if handle.column_info.any?
|
826
|
+
pk_index = handle.detect {|index| index[1] =~ /primary key/ }
|
827
|
+
keys << pk_index[2] if pk_index
|
828
|
+
end
|
829
|
+
end
|
830
|
+
keys.length == 1 ? [keys.first, nil] : nil
|
831
|
+
ensure
|
832
|
+
@connection["AutoCommit"] = true
|
833
|
+
end
|
834
|
+
|
835
|
+
private
|
836
|
+
def __indexes(table_name, name = nil)
|
837
|
+
indexes = []
|
838
|
+
execute("EXEC sp_helpindex '#{table_name}'", name) do |handle|
|
839
|
+
if handle.column_info.any?
|
840
|
+
handle.each do |index|
|
841
|
+
unique = index[1] =~ /unique/
|
842
|
+
primary = index[1] =~ /primary key/
|
843
|
+
if !primary
|
844
|
+
indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", ").map {|e| e.gsub('(-)','')})
|
845
|
+
end
|
846
|
+
end
|
847
|
+
end
|
848
|
+
end
|
849
|
+
indexes
|
850
|
+
end
|
851
|
+
|
852
|
+
def select(sql, name = nil, ignore_special_columns = false)
|
853
|
+
repair_special_columns(sql) unless ignore_special_columns
|
854
|
+
result = []
|
855
|
+
execute(sql) do |handle|
|
856
|
+
handle.each do |row|
|
857
|
+
row_hash = {}
|
858
|
+
row.each_with_index do |value, i|
|
859
|
+
if value.is_a? DBI::Timestamp
|
860
|
+
value = DateTime.new(value.year, value.month, value.day, value.hour, value.minute, value.sec)
|
861
|
+
end
|
862
|
+
row_hash[handle.column_names[i]] = value
|
863
|
+
end
|
864
|
+
result << row_hash
|
865
|
+
end
|
866
|
+
end
|
867
|
+
result
|
868
|
+
end
|
869
|
+
|
870
|
+
# Turns IDENTITY_INSERT ON for table during execution of the block
|
871
|
+
# N.B. This sets the state of IDENTITY_INSERT to OFF after the
|
872
|
+
# block has been executed without regard to its previous state
|
873
|
+
|
874
|
+
def with_identity_insert_enabled(table_name, &block)
|
875
|
+
set_identity_insert(table_name, true)
|
876
|
+
yield
|
877
|
+
ensure
|
878
|
+
set_identity_insert(table_name, false)
|
879
|
+
end
|
880
|
+
|
881
|
+
def set_identity_insert(table_name, enable = true)
|
882
|
+
execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
|
883
|
+
rescue Exception => e
|
884
|
+
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
|
885
|
+
end
|
886
|
+
|
887
|
+
def get_table_name(sql)
|
888
|
+
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
889
|
+
$1 || $2
|
890
|
+
elsif sql =~ /from\s+([^\(\s]+)\s*/i
|
891
|
+
$1
|
892
|
+
else
|
893
|
+
nil
|
894
|
+
end
|
895
|
+
end
|
896
|
+
|
897
|
+
def identity_column(table_name)
|
898
|
+
@table_columns ||= {}
|
899
|
+
@table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
|
900
|
+
@table_columns[table_name].each do |col|
|
901
|
+
return col.name if col.identity
|
902
|
+
end
|
903
|
+
|
904
|
+
return nil
|
905
|
+
end
|
906
|
+
|
907
|
+
def query_requires_identity_insert?(sql)
|
908
|
+
table_name = get_table_name(sql)
|
909
|
+
id_column = identity_column(table_name)
|
910
|
+
sql =~ /INSERT[^(]+\([^)]*\[#{id_column}\][^)]*\)/ ? table_name : nil
|
911
|
+
end
|
912
|
+
|
913
|
+
def change_order_direction(order)
|
914
|
+
order.split(",").collect {|fragment|
|
915
|
+
case fragment
|
916
|
+
when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
|
917
|
+
when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
|
918
|
+
else String.new(fragment).split(',').join(' DESC,') + ' DESC'
|
919
|
+
end
|
920
|
+
}.join(",")
|
921
|
+
end
|
922
|
+
|
923
|
+
def get_special_columns(table_name)
|
924
|
+
special = []
|
925
|
+
@table_columns ||= {}
|
926
|
+
@table_columns[table_name] ||= columns(table_name)
|
927
|
+
@table_columns[table_name].each do |col|
|
928
|
+
special << col.name if col.is_special
|
929
|
+
end
|
930
|
+
special
|
931
|
+
end
|
932
|
+
|
933
|
+
def repair_special_columns(sql)
|
934
|
+
special_cols = get_special_columns(get_table_name(sql))
|
935
|
+
for col in special_cols.to_a
|
936
|
+
sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
|
937
|
+
sql.gsub!(/ORDER BY #{col.to_s}/i, '')
|
938
|
+
end
|
939
|
+
sql
|
940
|
+
end
|
941
|
+
|
942
|
+
def get_utf8_columns(table_name)
|
943
|
+
utf8 = []
|
944
|
+
@table_columns ||= {}
|
945
|
+
@table_columns[table_name] ||= columns(table_name)
|
946
|
+
@table_columns[table_name].each do |col|
|
947
|
+
utf8 << col.name if col.is_utf8
|
948
|
+
end
|
949
|
+
utf8
|
950
|
+
end
|
951
|
+
|
952
|
+
def set_utf8_values!(sql)
|
953
|
+
utf8_cols = get_utf8_columns(get_table_name(sql))
|
954
|
+
if sql =~ /^\s*UPDATE/i
|
955
|
+
utf8_cols.each do |col|
|
956
|
+
sql.gsub!("[#{col.to_s}] = '", "[#{col.to_s}] = N'")
|
957
|
+
end
|
958
|
+
elsif sql =~ /^\s*INSERT(?!.*DEFAULT VALUES\s*$)/i
|
959
|
+
# TODO This code should be simplified
|
960
|
+
# Get columns and values, split them into arrays, and store the original_values for when we need to replace them
|
961
|
+
columns_and_values = sql.scan(/\((.*?)\)/m).flatten
|
962
|
+
columns = columns_and_values.first.split(',')
|
963
|
+
values = columns_and_values[1].split(',')
|
964
|
+
original_values = values.dup
|
965
|
+
# Iterate columns that should be UTF8, and append an N to the value, if the value is not NULL
|
966
|
+
utf8_cols.each do |col|
|
967
|
+
columns.each_with_index do |column, idx|
|
968
|
+
values[idx] = " N#{values[idx].gsub(/^ /, '')}" if column =~ /\[#{col}\]/ and values[idx] !~ /^NULL$/
|
969
|
+
end
|
970
|
+
end
|
971
|
+
# Replace (in place) the SQL
|
972
|
+
sql.gsub!(original_values.join(','), values.join(','))
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
end #class SQLServerAdapter < AbstractAdapter
|
977
|
+
end #module ConnectionAdapters
|
978
|
+
end #module ActiveRecord
|
979
|
+
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ebryn-activerecord-sqlserver-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shawn Balestracci
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-09-27 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.15.5.7843
|
23
|
+
version:
|
24
|
+
description:
|
25
|
+
email: shawn@vegantech.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- lib/active_record/connection_adapters/sqlserver_adapter.rb
|
34
|
+
has_rdoc: false
|
35
|
+
homepage: http://vegantech.lighthouseapp.com/projects/17542-activerecord-sqlserver-adapter
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project: activerecord
|
56
|
+
rubygems_version: 1.2.0
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: SQL Server adapter for Active Record
|
60
|
+
test_files: []
|
61
|
+
|