rails-sqlserver-2000-2005-adapter 2.2.15 → 2.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,7 +1,25 @@
1
1
 
2
2
  MASTER
3
3
 
4
- *
4
+ *
5
+
6
+
7
+ * 2.2.16 * (April 21st, 2009)
8
+
9
+ * Make add_limit_offset! only add locking hints (for tally) when the :lock option is present. Added tests
10
+ to make sure tally SQL is augmented correctly and tests to make sure that add_lock! is doing what it needs
11
+ for deep sub selects in paginated results. [Ken Collins]
12
+
13
+ * Add auto reconnect support utilizing a new #with_auto_reconnect block. By default each query run through
14
+ the adapter will automatically reconnect at standard intervals, logging attempts along the way, till success
15
+ or the original exception bubbles up. See docs for more details. Resolves ticket #18 [Ken Collins]
16
+
17
+ * Update internal helper method #orders_and_dirs_set to cope with an order clause like "description desc". This
18
+ resolves ticket #26 [Ken Collins]
19
+
20
+ * Provide support for running queries at different isolation levels using #run_with_isolation_level method
21
+ that can take a block or not. Also implement a #user_options method that reflects on the current user
22
+ session values. Resolves #20 [Murray Steele]
5
23
 
6
24
 
7
25
  * 2.2.15 * (March 23rd, 2009)
data/README.rdoc CHANGED
@@ -16,6 +16,8 @@ The SQL Server adapter for rails is back for ActiveRecord 2.2 and up! We are cur
16
16
  * Enabled #case_sensitive_equality_operator used by unique validations.
17
17
  * Unicode character support for nchar, nvarchar and ntext data types. Configuration option for defaulting all string data types to the unicode safe types.
18
18
  * View support for table names, identity inserts, and column defaults.
19
+ * A block method to run queries within a specific isolation level.
20
+ * Automatically reconnects to lost database connections.
19
21
 
20
22
  ==== Date/Time Data Type Hinting
21
23
 
@@ -103,6 +105,12 @@ By default all queries to the INFORMATION_SCHEMA table is silenced. If you think
103
105
 
104
106
  ActiveRecord::ConnectionAdapters::SQLServerAdapter.log_info_schema_queries = true
105
107
 
108
+ ==== Auto Connecting
109
+
110
+ By default the adapter will auto connect to lost DB connections. For every query it will retry at intervals of 2, 4, 8, 16 and 32 seconds. During each retry it will callback out to ActiveRecord::Base.did_retry_sqlserver_connection(connection,count). When all retries fail, it will callback to ActiveRecord::Base.did_lose_sqlserver_connection(connection). Both implementations of these methods are to write to the rails logger, however, they make great override points for notifications like Hoptoad. If you want to disable automatic reconnections use the following in an initializer.
111
+
112
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = false
113
+
106
114
 
107
115
  == Versions
108
116
 
@@ -23,9 +23,17 @@ module ActiveRecord
23
23
  host = config[:host] ? config[:host].to_s : 'localhost'
24
24
  driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
25
25
  end
26
- conn = DBI.connect(driver_url, username, password)
27
- conn["AutoCommit"] = true
28
- ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
26
+ ConnectionAdapters::SQLServerAdapter.new(logger, [driver_url, username, password])
27
+ end
28
+
29
+ protected
30
+
31
+ def self.did_retry_sqlserver_connection(connection,count)
32
+ logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}."
33
+ end
34
+
35
+ def self.did_lose_sqlserver_connection(connection)
36
+ logger.info "CONNECTION LOST: #{connection.class.name}"
29
37
  end
30
38
 
31
39
  end
@@ -150,13 +158,20 @@ module ActiveRecord
150
158
  class SQLServerAdapter < AbstractAdapter
151
159
 
152
160
  ADAPTER_NAME = 'SQLServer'.freeze
153
- VERSION = '2.2.15'.freeze
161
+ VERSION = '2.2.16'.freeze
154
162
  DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
155
163
  SUPPORTED_VERSIONS = [2000,2005].freeze
156
164
  LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
157
165
 
166
+ LOST_CONNECTION_EXCEPTIONS = [DBI::DatabaseError, DBI::InterfaceError]
167
+ LOST_CONNECTION_MESSAGES = [
168
+ 'Communication link failure',
169
+ 'Read from the server failed',
170
+ 'Write to the server failed',
171
+ 'Database connection was already closed']
172
+
158
173
  cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
159
- :log_info_schema_queries, :enable_default_unicode_types
174
+ :log_info_schema_queries, :enable_default_unicode_types, :auto_connect
160
175
 
161
176
  class << self
162
177
 
@@ -166,9 +181,10 @@ module ActiveRecord
166
181
 
167
182
  end
168
183
 
169
- def initialize(connection, logger, connection_options=nil)
170
- super(connection, logger)
184
+ def initialize(logger, connection_options)
171
185
  @connection_options = connection_options
186
+ connect
187
+ super(raw_connection, logger)
172
188
  initialize_sqlserver_caches
173
189
  unless SUPPORTED_VERSIONS.include?(database_year)
174
190
  raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
@@ -221,6 +237,10 @@ module ActiveRecord
221
237
  "#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
222
238
  end
223
239
 
240
+ def auto_connect
241
+ @@auto_connect.is_a?(FalseClass) ? false : true
242
+ end
243
+
224
244
  def native_string_database_type
225
245
  @@native_string_database_type || (enable_default_unicode_types ? 'nvarchar' : 'varchar')
226
246
  end
@@ -302,16 +322,14 @@ module ActiveRecord
302
322
  def active?
303
323
  raw_connection.execute("SELECT 1").finish
304
324
  true
305
- rescue DBI::DatabaseError, DBI::InterfaceError
325
+ rescue *LOST_CONNECTION_EXCEPTIONS
306
326
  false
307
327
  end
308
328
 
309
329
  def reconnect!
310
330
  disconnect!
311
- @connection = DBI.connect(*@connection_options)
312
- rescue DBI::DatabaseError => e
313
- @logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
314
- false
331
+ connect
332
+ active?
315
333
  end
316
334
 
317
335
  def disconnect!
@@ -325,6 +343,30 @@ module ActiveRecord
325
343
 
326
344
  # DATABASE STATEMENTS ======================================#
327
345
 
346
+ def user_options
347
+ info_schema_query do
348
+ select_rows("dbcc useroptions").inject(HashWithIndifferentAccess.new) do |values,row|
349
+ set_option = row[0].gsub(/\s+/,'_')
350
+ user_value = row[1]
351
+ values[set_option] = user_value
352
+ values
353
+ end
354
+ end
355
+ end
356
+
357
+ VALID_ISOLATION_LEVELS = ["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
358
+
359
+ def run_with_isolation_level(isolation_level)
360
+ raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{VALID_ISOLATION_LEVELS.to_sentence}." if !VALID_ISOLATION_LEVELS.include?(isolation_level.upcase)
361
+ initial_isolation_level = user_options[:isolation_level] || "READ COMMITTED"
362
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
363
+ begin
364
+ yield
365
+ ensure
366
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
367
+ end if block_given?
368
+ end
369
+
328
370
  def select_rows(sql, name = nil)
329
371
  raw_select(sql,name).last
330
372
  end
@@ -395,7 +437,7 @@ module ActiveRecord
395
437
  # The business of adding limit/offset
396
438
  if options[:limit] and options[:offset]
397
439
  tally_sql = "SELECT count(*) as TotalRows from (#{sql.sub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally"
398
- add_lock! tally_sql, :lock => 'WITH (NOLOCK)'
440
+ add_lock! tally_sql, options
399
441
  total_rows = select_value(tally_sql).to_i
400
442
  if (options[:limit] + options[:offset]) >= total_rows
401
443
  options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
@@ -697,6 +739,47 @@ module ActiveRecord
697
739
 
698
740
  protected
699
741
 
742
+ # CONNECTION MANAGEMENT ====================================#
743
+
744
+ def connect
745
+ driver_url, username, password = @connection_options
746
+ @connection = DBI.connect(driver_url, username, password)
747
+ configure_connection
748
+ rescue
749
+ raise unless @auto_connecting
750
+ end
751
+
752
+ def configure_connection
753
+ raw_connection['AutoCommit'] = true
754
+ end
755
+
756
+ def with_auto_reconnect
757
+ begin
758
+ yield
759
+ rescue *LOST_CONNECTION_EXCEPTIONS => e
760
+ if LOST_CONNECTION_MESSAGES.any? { |lcm| e.message =~ Regexp.new(lcm,Regexp::IGNORECASE) }
761
+ retry if auto_reconnected?
762
+ end
763
+ raise
764
+ end
765
+ end
766
+
767
+ def auto_reconnected?
768
+ return false unless auto_connect
769
+ @auto_connecting = true
770
+ count = 0
771
+ while count <= 5
772
+ sleep 2** count
773
+ ActiveRecord::Base.did_retry_sqlserver_connection(self,count)
774
+ return true if reconnect!
775
+ count += 1
776
+ end
777
+ ActiveRecord::Base.did_lose_sqlserver_connection(self)
778
+ false
779
+ ensure
780
+ @auto_connecting = false
781
+ end
782
+
700
783
  # DATABASE STATEMENTS ======================================
701
784
 
702
785
  def select(sql, name = nil, ignore_special_columns = false)
@@ -727,9 +810,9 @@ module ActiveRecord
727
810
  def raw_execute(sql, name = nil, &block)
728
811
  log(sql, name) do
729
812
  if block_given?
730
- raw_connection.execute(sql) { |handle| yield(handle) }
813
+ with_auto_reconnect { raw_connection.execute(sql) { |handle| yield(handle) } }
731
814
  else
732
- raw_connection.execute(sql)
815
+ with_auto_reconnect { raw_connection.execute(sql) }
733
816
  end
734
817
  end
735
818
  end
@@ -743,7 +826,7 @@ module ActiveRecord
743
826
 
744
827
  def do_execute(sql,name=nil)
745
828
  log(sql, name || 'EXECUTE') do
746
- raw_connection.do(sql)
829
+ with_auto_reconnect { raw_connection.do(sql) }
747
830
  end
748
831
  end
749
832
 
@@ -877,12 +960,13 @@ module ActiveRecord
877
960
  orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
878
961
  orders_dirs = orders.map do |ord|
879
962
  dir = nil
880
- if match_data = ord.match(/\b(asc|desc)$/i)
881
- dir = match_data[1]
882
- ord.sub!(dir,'').strip!
883
- dir.upcase!
963
+ ord.sub!(/\b(asc|desc)$/i) do |match|
964
+ if match
965
+ dir = match.upcase.strip
966
+ ''
967
+ end
884
968
  end
885
- [ord,dir]
969
+ [ord.strip, dir]
886
970
  end
887
971
  end
888
972
 
@@ -5,6 +5,8 @@ require 'models/joke'
5
5
  require 'models/subscriber'
6
6
 
7
7
  class AdapterTestSqlserver < ActiveRecord::TestCase
8
+
9
+ fixtures :tasks
8
10
 
9
11
  def setup
10
12
  @connection = ActiveRecord::Base.connection
@@ -376,6 +378,78 @@ class AdapterTestSqlserver < ActiveRecord::TestCase
376
378
 
377
379
  context 'For DatabaseStatements' do
378
380
 
381
+ context "finding out what user_options are available" do
382
+
383
+ should "run the database consistency checker useroptions command" do
384
+ @connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns []
385
+ @connection.user_options
386
+ end
387
+
388
+ should "return a underscored key hash with indifferent access of the results" do
389
+ @connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns [['some', 'thing'], ['isolation level', 'read uncommitted']]
390
+ uo = @connection.user_options
391
+ assert_equal 2, uo.keys.size
392
+ assert_equal 'thing', uo['some']
393
+ assert_equal 'thing', uo[:some]
394
+ assert_equal 'read uncommitted', uo['isolation_level']
395
+ assert_equal 'read uncommitted', uo[:isolation_level]
396
+ end
397
+
398
+ end
399
+
400
+ context "altering isolation levels" do
401
+
402
+ should "barf if the requested isolation level is not valid" do
403
+ assert_raise(ArgumentError) do
404
+ @connection.run_with_isolation_level 'INVALID ISOLATION LEVEL' do; end
405
+ end
406
+ end
407
+
408
+ context "with a valid isolation level" do
409
+
410
+ setup do
411
+ @t1 = tasks(:first_task)
412
+ @t2 = tasks(:another_task)
413
+ assert @t1, 'Tasks :first_task should be in AR fixtures'
414
+ assert @t2, 'Tasks :another_task should be in AR fixtures'
415
+ good_isolation_level = @connection.user_options[:isolation_level].blank? || @connection.user_options[:isolation_level] =~ /read committed/i
416
+ assert good_isolation_level, "User isolation level is not at a happy starting place: #{@connection.user_options[:isolation_level].inspect}"
417
+ end
418
+
419
+ should 'allow #run_with_isolation_level to not take a block to set it' do
420
+ begin
421
+ @connection.run_with_isolation_level 'READ UNCOMMITTED'
422
+ assert_match %r|read uncommitted|i, @connection.user_options[:isolation_level]
423
+ ensure
424
+ @connection.run_with_isolation_level 'READ COMMITTED'
425
+ end
426
+ end
427
+
428
+ should 'return block value using #run_with_isolation_level' do
429
+ assert_same_elements Task.find(:all), @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(:all) }
430
+ end
431
+
432
+ should 'pass a read uncommitted isolation level test' do
433
+ assert_nil @t2.starting, 'Fixture should have this empty.'
434
+ begin
435
+ Task.transaction do
436
+ @t2.starting = Time.now
437
+ @t2.save
438
+ @dirty_t2 = @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(@t2.id) }
439
+ raise ActiveRecord::ActiveRecordError
440
+ end
441
+ rescue
442
+ 'Do Nothing'
443
+ end
444
+ assert @dirty_t2, 'Should have a Task record from within block above.'
445
+ assert @dirty_t2.starting, 'Should have a dirty date.'
446
+ assert_nil Task.find(@t2.id).starting, 'Should be nil again from botched transaction above.'
447
+ end unless active_record_2_point_2? # Transactions in tests are a bit screwy in 2.2.
448
+
449
+ end
450
+
451
+ end
452
+
379
453
  end
380
454
 
381
455
  context 'For SchemaStatements' do
@@ -78,6 +78,37 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
78
78
  end
79
79
  end
80
80
 
81
+ context 'Connection management' do
82
+
83
+ setup do
84
+ assert @connection.active?
85
+ end
86
+
87
+ should 'be able to disconnect and reconnect at will' do
88
+ @connection.disconnect!
89
+ assert !@connection.active?
90
+ @connection.reconnect!
91
+ assert @connection.active?
92
+ end
93
+
94
+ should 'auto reconnect when setting is on' do
95
+ with_auto_connect(true) do
96
+ @connection.disconnect!
97
+ assert_nothing_raised() { Topic.count }
98
+ assert @connection.active?
99
+ end
100
+ end
101
+
102
+ should 'not auto reconnect when setting is off' do
103
+ with_auto_connect(false) do
104
+ @connection.disconnect!
105
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.count }
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+
81
112
 
82
113
  private
83
114
 
@@ -99,5 +130,13 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
99
130
  ensure
100
131
  GC.enable
101
132
  end
133
+
134
+ def with_auto_connect(boolean)
135
+ existing = ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect
136
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = boolean
137
+ yield
138
+ ensure
139
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = existing
140
+ end
102
141
 
103
142
  end
@@ -66,9 +66,16 @@ class OffsetAndLimitTestSqlserver < ActiveRecord::TestCase
66
66
 
67
67
  should 'alter SQL to limit number of records returned offset by specified amount' do
68
68
  options = { :limit => 3, :offset => 5 }
69
- expected_sql = %&SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books) AS tmp1) AS tmp2&
69
+ expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books) AS tmp1) AS tmp2"
70
70
  assert_equal(expected_sql, @connection.add_limit_offset!(@select_sql, options))
71
71
  end
72
+
73
+ should 'add locks to deepest sub select in limit offset sql that has a limited tally' do
74
+ options = { :limit => 3, :offset => 5, :lock => 'WITH (NOLOCK)' }
75
+ expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books WITH (NOLOCK)) AS tmp1) AS tmp2"
76
+ @connection.add_limit_offset! @select_sql, options
77
+ assert_equal expected_sql, @connection.add_lock!(@select_sql,options)
78
+ end
72
79
 
73
80
  # Not really sure what an offset sql injection might look like
74
81
  should 'not allow sql injection via offset' do
@@ -82,6 +89,18 @@ class OffsetAndLimitTestSqlserver < ActiveRecord::TestCase
82
89
  assert_equal expected_sql, @connection.add_limit_offset!(@subquery_select_sql,options)
83
90
  end
84
91
 
92
+ should 'add lock hints to tally sql if :lock option is present' do
93
+ assert_sql %r|SELECT TOP 1000000000 \* FROM \[people\] WITH \(NOLOCK\)| do
94
+ Person.all :limit => 5, :offset => 1, :lock => 'WITH (NOLOCK)'
95
+ end
96
+ end
97
+
98
+ should 'not add lock hints to tally sql if there is no :lock option' do
99
+ assert_sql %r|\(SELECT TOP 1000000000 \* FROM \[people\] \)| do
100
+ Person.all :limit => 5, :offset => 1
101
+ end
102
+ end
103
+
85
104
  end
86
105
 
87
106
 
@@ -1,6 +1,7 @@
1
1
  require 'cases/sqlserver_helper'
2
2
 
3
3
  class StringDefault < ActiveRecord::Base; end;
4
+ class SqlServerEdgeSchema < ActiveRecord::Base; end;
4
5
 
5
6
  class SpecificSchemaTestSqlserver < ActiveRecord::TestCase
6
7
 
@@ -23,4 +24,34 @@ class SpecificSchemaTestSqlserver < ActiveRecord::TestCase
23
24
  assert_equal '(NULL)', default.string_with_pretend_null_four
24
25
  end
25
26
 
27
+ context 'Testing edge case schemas' do
28
+
29
+ setup do
30
+ @edge_class = SqlServerEdgeSchema
31
+ end
32
+
33
+ context 'with description column' do
34
+
35
+ setup do
36
+ @da = @edge_class.create! :description => 'A'
37
+ @db = @edge_class.create! :description => 'B'
38
+ @dc = @edge_class.create! :description => 'C'
39
+ end
40
+
41
+ teardown { @edge_class.delete_all }
42
+
43
+ should 'allow all sorts of ordering without adapter munging it up' do
44
+ assert_equal ['A','B','C'], @edge_class.all(:order => 'description').map(&:description)
45
+ assert_equal ['A','B','C'], @edge_class.all(:order => 'description asc').map(&:description)
46
+ assert_equal ['A','B','C'], @edge_class.all(:order => 'description ASC').map(&:description)
47
+ assert_equal ['C','B','A'], @edge_class.all(:order => 'description desc').map(&:description)
48
+ assert_equal ['C','B','A'], @edge_class.all(:order => 'description DESC').map(&:description)
49
+ end
50
+
51
+ end
52
+
53
+
54
+ end
55
+
56
+
26
57
  end
@@ -63,6 +63,10 @@ ActiveRecord::Schema.define do
63
63
  # TODO: Add some different native binary types and test.
64
64
  end
65
65
 
66
+ create_table :sql_server_edge_schemas, :force => true do |t|
67
+ t.string :description
68
+ end
69
+
66
70
  execute "IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = 'customers_view') DROP VIEW customers_view"
67
71
  execute <<-CUSTOMERSVIEW
68
72
  CREATE VIEW customers_view AS
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-sqlserver-2000-2005-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.15
4
+ version: 2.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken Collins
@@ -13,7 +13,7 @@ autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
15
 
16
- date: 2009-03-23 00:00:00 -07:00
16
+ date: 2009-04-21 00:00:00 -07:00
17
17
  default_executable:
18
18
  dependencies: []
19
19