vegantech-activerecord-sqlserver-adapter 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+