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