Changeset 4601
- Timestamp:
- 07/10/06 18:24:35 (2 years ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb (modified) (19 diffs)
- trunk/activerecord/Rakefile (modified) (1 diff)
- trunk/activerecord/test/adapter_test_sqlserver.rb (added)
- trunk/activerecord/test/locking_test.rb (modified) (1 diff)
- trunk/activerecord/test/migration_test.rb (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r4600 r4601 1 1 *SVN* 2 3 * SQLServer: added tests to ensure all database statements are closed, refactored identity_insert management code to use blocks, removed update/delete rowcount code out of execute and into update/delete, changed insert to go through execute method, removed unused quoting methods, disabled pessimistic locking tests as feature is currently unsupported, fixed RakeFile to load sqlserver specific tests whether running in ado or odbc mode, fixed support for recently added decimal types, added support for limits on integer types. #5670 [Tom Ward] 4 5 * SQLServer: fix db:schema:dump case-sensitivity. #4684 [Will Rogers] 2 6 3 7 * Oracle: BigDecimal support. #5667 [schoenm@earthlink.net] trunk/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
r4596 r4601 14 14 # Modifications (ODBC): Mark Imbriaco <mark.imbriaco@pobox.com> 15 15 # Date: 6/26/2005 16 # 17 # Current maintainer: Ryan Tomayko <rtomayko@gmail.com> 18 # 16 19 17 # Modifications (Migrations): Tom Ward <tom@popdog.net> 20 18 # Date: 27/10/2005 21 19 # 20 # Modifications (Numerous fixes as maintainer): Ryan Tomayko <rtomayko@gmail.com> 21 # Date: Up to July 2006 22 23 # Current maintainer: Tom Ward <tom@popdog.net> 22 24 23 25 module ActiveRecord … … 82 84 end 83 85 end 84 86 85 87 def cast_to_time(value) 86 88 return value if value.is_a?(Time) … … 90 92 91 93 def cast_to_datetime(value) 94 return value.to_time if value.is_a?(DBI::Timestamp) 95 92 96 if value.is_a?(Time) 93 97 if value.year != 0 and value.month != 0 and value.day != 0 … … 97 101 end 98 102 end 103 104 if value.is_a?(DateTime) 105 return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec) 106 end 107 99 108 return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil 100 109 value 110 end 111 112 # TODO: Find less hack way to convert DateTime objects into Times 113 114 def self.string_to_time(value) 115 if value.is_a?(DateTime) 116 return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec) 117 else 118 super 119 end 101 120 end 102 121 … … 185 204 :time => { :name => "datetime" }, 186 205 :date => { :name => "datetime" }, 187 :binary => { :name => "image" },188 :boolean => { :name => "bit" }206 :binary => { :name => "image"}, 207 :boolean => { :name => "bit"} 189 208 } 190 209 end … … 196 215 def supports_migrations? #:nodoc: 197 216 true 217 end 218 219 def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: 220 return super unless type.to_s == 'integer' 221 222 if limit.nil? || limit == 4 223 'integer' 224 elsif limit < 4 225 'smallint' 226 else 227 'bigint' 228 end 198 229 end 199 230 … … 237 268 table_name = table_name.to_s if table_name.is_a?(Symbol) 238 269 table_name = table_name.split('.')[-1] unless table_name.nil? 239 sql = "SELECT COLUMN_NAME as ColName, 240 COLUMN_DEFAULT as DefaultValue, 241 DATA_TYPE as ColType, 242 IS_NULLABLE As IsNullable, 243 COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, 244 COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, 245 NUMERIC_PRECISION as [Precision], 246 NUMERIC_SCALE as Scale 247 FROM INFORMATION_SCHEMA.COLUMNS 248 WHERE TABLE_NAME = '#{table_name}'" 270 sql = %Q{ 271 SELECT 272 cols.COLUMN_NAME as ColName, 273 cols.COLUMN_DEFAULT as DefaultValue, 274 cols.NUMERIC_SCALE as numeric_scale, 275 cols.NUMERIC_PRECISION as numeric_precision, 276 cols.DATA_TYPE as ColType, 277 cols.IS_NULLABLE As IsNullable, 278 COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) as Length, 279 COLUMNPROPERTY(OBJECT_ID(cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') as IsIdentity, 280 cols.NUMERIC_SCALE as Scale 281 FROM INFORMATION_SCHEMA.COLUMNS cols 282 WHERE cols.TABLE_NAME = '#{table_name}' 283 } 249 284 # Comment out if you want to have the Columns select statment logged. 250 285 # Personally, I think it adds unnecessary bloat to the log. … … 256 291 default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue] 257 292 if field[:ColType] =~ /numeric|decimal/i 258 type = "#{field[:ColType]}(#{field[: Precision]},#{field[:Scale]})"293 type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})" 259 294 else 260 295 type = "#{field[:ColType]}(#{field[:Length]})" … … 268 303 269 304 def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) 270 begin 271 table_name = get_table_name(sql) 272 col = get_identity_column(table_name) 273 ii_enabled = false 274 275 if col != nil 276 if query_contains_identity_column(sql, col) 277 begin 278 execute enable_identity_insert(table_name, true) 279 ii_enabled = true 280 rescue Exception => e 281 raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON" 305 execute(sql, name) 306 id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"] 307 end 308 309 def update(sql, name = nil) 310 execute(sql, name) do |handle| 311 handle.rows 312 end || select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"] 313 end 314 315 alias_method :delete, :update 316 317 def execute(sql, name = nil) 318 if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql)) 319 log(sql, name) do 320 with_identity_insert_enabled(table_name) do 321 @connection.execute(sql) do |handle| 322 yield(handle) if block_given? 282 323 end 283 324 end 284 325 end 285 log(sql, name) do286 @connection.execute(sql).finish287 id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"]288 end289 ensure290 if ii_enabled291 begin292 execute enable_identity_insert(table_name, false)293 rescue Exception => e294 raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF"295 end296 end297 end298 end299 300 def execute(sql, name = nil)301 if sql =~ /^\s*INSERT/i302 insert(sql, name)303 elsif sql =~ /^\s*UPDATE|^\s*DELETE/i304 log(sql, name) do305 ret = @connection.execute(sql).finish306 retVal = select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"]307 end308 326 else 309 327 log(sql, name) do 310 if block_given? 311 @connection.execute(sql) do |sth| 312 yield(sth) 313 end 314 else 315 @connection.execute(sql).finish 328 @connection.execute(sql) do |handle| 329 yield(handle) if block_given? 316 330 end 317 331 end 318 332 end 319 333 end 320 321 def update(sql, name = nil) 322 execute(sql, name) 323 end 324 alias_method :delete, :update 325 334 326 335 def begin_db_transaction 327 336 @connection["AutoCommit"] = false … … 355 364 def quote_string(string) 356 365 string.gsub(/\'/, "''") 357 end358 359 def quoted_true360 "1"361 end362 363 def quoted_false364 "0"365 366 end 366 367 … … 421 422 422 423 def tables(name = nil) 423 execute("SELECT table_name from information_schema.tables WHERE table_type= 'BASE TABLE'", name) do |sth|424 execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", name) do |sth| 424 425 sth.inject([]) do |tables, field| 425 426 table_name = field[0] … … 451 452 end 452 453 453 def remove_column(table_name, column_name) 454 execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}" 455 end 456 454 # Adds a new column to the named table. 455 # See TableDefinition#column for details of the options you can use. 456 def add_column(table_name, column_name, type, options = {}) 457 add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" 458 add_column_options!(add_column_sql, options) 459 # TODO: Add support to mimic date columns, using constraints to mark them as such in the database 460 # 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 461 execute(add_column_sql) 462 end 463 457 464 def rename_column(table, column, new_column_name) 458 465 execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" … … 461 468 def change_column(table_name, column_name, type, options = {}) #:nodoc: 462 469 sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"] 463 if options[:default]470 unless options[:default].nil? 464 471 remove_default_constraint(table_name, column_name) 465 sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{ options[:default]} FOR #{column_name}"472 sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default])} FOR #{column_name}" 466 473 end 467 474 sql_commands.each {|c| … … 471 478 472 479 def remove_column(table_name, column_name) 480 remove_check_constraints(table_name, column_name) 473 481 remove_default_constraint(table_name, column_name) 474 execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"482 execute "ALTER TABLE [#{table_name}] DROP COLUMN [#{column_name}]" 475 483 end 476 484 477 485 def remove_default_constraint(table_name, column_name) 478 defaults = 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" 479 defaults.each {|constraint| 486 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" 487 488 constraints.each do |constraint| 480 489 execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" 481 } 490 end 491 end 492 493 def remove_check_constraints(table_name, column_name) 494 # TODO remove all constraints in single method 495 constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'" 496 constraints.each do |constraint| 497 execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}" 498 end 482 499 end 483 500 … … 486 503 end 487 504 488 private 505 private 489 506 def select(sql, name = nil) 490 507 rows = [] … … 495 512 row.column_names.each do |col| 496 513 record[col] = row[col] 497 record[col] = record[col].to_time if record[col].is_a? DBI::Timestamp 514 if record[col].is_a? DBI::Timestamp 515 ts = record[col] 516 record[col] = DateTime.new(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.sec) 517 end 498 518 end 499 519 rows << record … … 503 523 end 504 524 505 def enable_identity_insert(table_name, enable = true) 506 if has_identity_column(table_name) 507 "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" 508 end 525 # Turns IDENTITY_INSERT ON for table during execution of the block 526 # N.B. This sets the state of IDENTITY_INSERT to OFF after the 527 # block has been executed without regard to its previous state 528 529 def with_identity_insert_enabled(table_name, &block) 530 set_identity_insert(table_name, true) 531 yield 532 ensure 533 set_identity_insert(table_name, false) 534 end 535 536 def set_identity_insert(table_name, enable = true) 537 execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" 538 rescue Exception => e 539 raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}" 509 540 end 510 541 … … 519 550 end 520 551 521 def has_identity_column(table_name) 522 !get_identity_column(table_name).nil? 523 end 524 525 def get_identity_column(table_name) 552 def identity_column(table_name) 526 553 @table_columns = {} unless @table_columns 527 554 @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil … … 533 560 end 534 561 535 def query_contains_identity_column(sql, col) 536 sql =~ /\[#{col}\]/ 562 def query_requires_identity_insert?(sql) 563 table_name = get_table_name(sql) 564 id_column = identity_column(table_name) 565 sql =~ /\[#{id_column}\]/ ? table_name : nil 537 566 end 538 567 trunk/activerecord/Rakefile
r4588 r4601 31 31 Rake::TestTask.new("test_#{adapter}") { |t| 32 32 t.libs << "test" << "test/connections/native_#{adapter}" 33 t.pattern = "test/*_test{,_#{adapter}}.rb" 33 if adapter =~ /^sqlserver/ 34 t.pattern = "test/*_test{,_sqlserver}.rb" 35 else 36 t.pattern = "test/*_test{,_#{adapter}}.rb" 37 end 34 38 t.verbose = true 35 39 } trunk/activerecord/test/locking_test.rb
r4596 r4601 64 64 # blocks, so separate script called by Kernel#system is needed. 65 65 # (See exec vs. async_exec in the PostgreSQL adapter.) 66 class PessimisticLockingTest < Test::Unit::TestCase67 self.use_transactional_fixtures = false68 fixtures :people69 66 70 def setup 71 @allow_concurrency = ActiveRecord::Base.allow_concurrency 72 ActiveRecord::Base.allow_concurrency = true 73 end 67 # TODO: The SQL Server adapter currently has no support for pessimistic locking 74 68 75 def teardown 76 ActiveRecord::Base.allow_concurrency = @allow_concurrency 77 end 78 79 # Test that the adapter doesn't blow up on add_lock! 80 def test_sane_find_with_lock 81 assert_nothing_raised do 82 Person.transaction do 83 Person.find 1, :lock => true 84 end 69 unless current_adapter?(:SQLServerAdapter) 70 class PessimisticLockingTest < Test::Unit::TestCase 71 self.use_transactional_fixtures = false 72 fixtures :people 73 74 def setup 75 @allow_concurrency = ActiveRecord::Base.allow_concurrency 76 ActiveRecord::Base.allow_concurrency = true 85 77 end 86 end 87 88 # Test no-blowup for scoped lock. 89 def test_sane_find_with_lock 90 assert_nothing_raised do 91 Person.transaction do 92 Person.with_scope(:find => { :lock => true }) do 93 Person.find 1 78 79 def teardown 80 ActiveRecord::Base.allow_concurrency = @allow_concurrency 81 end 82 83 # Test that the adapter doesn't blow up on add_lock! 84 def test_sane_find_with_lock 85 assert_nothing_raised do 86 Person.transaction do 87 Person.find 1, :lock => true 94 88 end 95 89 end 96 90 end 97 end 98 99 # Locking a record reloads it. 100 def test_sane_lock_method 101 assert_nothing_raised do 102 Person.transaction do 103 person = Person.find 1 104 old, person.first_name = person.first_name, 'fooman' 105 person.lock! 106 assert_equal old, person.first_name 91 92 # Test no-blowup for scoped lock. 93 def test_sane_find_with_lock 94 assert_nothing_raised do 95 Person.transaction do 96 Person.with_scope(:find => { :lock => true }) do 97 Person.find 1 98 end 99 end 107 100 end 108 101 end 109 end 110 111 if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) 112 def test_no_locks_no_wait 113 first, second = duel { Person.find 1 } 114 assert first.end > second.end 102 103 # Locking a record reloads it. 104 def test_sane_lock_method 105 assert_nothing_raised do 106 Person.transaction do 107 person = Person.find 1 108 old, person.first_name = person.first_name, 'fooman' 109 person.lock! 110 assert_equal old, person.first_name 111 end 112 end 115 113 end 116 117 def test_second_lock_waits 118 assert [0.2, 1, 5].any? { |zzz| 119 first, second = duel(zzz) { Person.find 1, :lock => true } 120 second.end > first.end 121 } 114 115 if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) 116 def test_no_locks_no_wait 117 first, second = duel { Person.find 1 } 118 assert first.end > second.end 119 end 120 121 def test_second_lock_waits 122 assert [0.2, 1, 5].any? { |zzz| 123 first, second = duel(zzz) { Person.find 1, :lock => true } 124 second.end > first.end 125 } 126 end 127 128 protected 129 def duel(zzz = 5) 130 t0, t1, t2, t3 = nil, nil, nil, nil 131 132 a = Thread.new do 133 t0 = Time.now 134 Person.transaction do 135 yield 136 sleep zzz # block thread 2 for zzz seconds 137 end 138 t1 = Time.now 139 end 140 141 b = Thread.new do 142 sleep zzz / 2.0 # ensure thread 1 tx starts first 143 t2 = Time.now 144 Person.transaction { yield } 145 t3 = Time.now 146 end 147 148 a.join 149 b.join 150 151 assert t1 > t0 + zzz 152 assert t2 > t0 153 assert t3 > t2 154 [t0.to_f..t1.to_f, t2.to_f..t3.to_f] 155 end 122 156 end 123 124 protected125 def duel(zzz = 5)126 t0, t1, t2, t3 = nil, nil, nil, nil127 128 a = Thread.new do129 t0 = Time.now130 Person.transaction do131 yield132 sleep zzz # block thread 2 for zzz seconds133 end134 t1 = Time.now135 end136 137 b = Thread.new do138 sleep zzz / 2.0 # ensure thread 1 tx starts first139 t2 = Time.now140 Person.transaction { yield }141 t3 = Time.now142 end143 144 a.join145 b.join146 147 assert t1 > t0 + zzz148 assert t2 > t0149 assert t3 > t2150 [t0.to_f..t1.to_f, t2.to_f..t3.to_f]151 end152 157 end 153 158 end trunk/activerecord/test/migration_test.rb
r4600 r4601 40 40 Reminder.reset_column_information 41 41 42 %w(last_name key bio age height wealth birthday favorite_day male43 ma iladministrator).each do |column|42 %w(last_name key bio age height wealth birthday favorite_day 43 male administrator).each do |column| 44 44 Person.connection.remove_column('people', column) rescue nil 45 45 end … … 178 178 t.column :foo, :string 179 179 end 180 Person.connection.execute "insert into testings values (1, 'hello')" 180 181 con = Person.connection 182 Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" 181 183 assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" } 182 184 183 185 assert_raises(ActiveRecord::StatementInvalid) do 184 Person.connection.execute "insert into testings values (2, 'hello', NULL)"186 Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" 185 187 end 186 188 ensure … … 268 270 269 271 if current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter) 270 # S QL Server, Sybase, and Oracle don't differentiate between date/time272 # Sybase, and Oracle don't differentiate between date/time 271 273 assert_equal Time, bob.favorite_day.class 272 274 else … … 356 358 357 359 # Using explicit id in insert for compatibility across all databases 358 assert_nothing_raised { ActiveRecord::Base.connection.execute "INSERT INTO octopi VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" } 360 con = ActiveRecord::Base.connection 361 assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" } 359 362 360 363 assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1") … … 380 383 assert old_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } 381 384 assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => false } 382 new_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns") 385 new_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns") 383 386 assert_nil new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } 384 387 assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }