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
+