activerecord 1.14.4 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (159) hide show
  1. data/CHANGELOG +400 -1
  2. data/README +2 -2
  3. data/RUNNING_UNIT_TESTS +21 -3
  4. data/Rakefile +55 -10
  5. data/lib/active_record.rb +10 -4
  6. data/lib/active_record/acts/list.rb +15 -4
  7. data/lib/active_record/acts/nested_set.rb +11 -12
  8. data/lib/active_record/acts/tree.rb +13 -14
  9. data/lib/active_record/aggregations.rb +46 -22
  10. data/lib/active_record/associations.rb +213 -162
  11. data/lib/active_record/associations/association_collection.rb +45 -15
  12. data/lib/active_record/associations/association_proxy.rb +32 -13
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +18 -18
  14. data/lib/active_record/associations/has_many_association.rb +37 -17
  15. data/lib/active_record/associations/has_many_through_association.rb +120 -30
  16. data/lib/active_record/associations/has_one_association.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +75 -0
  18. data/lib/active_record/base.rb +282 -203
  19. data/lib/active_record/calculations.rb +95 -54
  20. data/lib/active_record/callbacks.rb +13 -24
  21. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +12 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb.rej +21 -0
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -4
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -9
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +121 -37
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -23
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
  28. data/lib/active_record/connection_adapters/db2_adapter.rb +1 -11
  29. data/lib/active_record/connection_adapters/firebird_adapter.rb +364 -50
  30. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  31. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -33
  32. data/lib/active_record/connection_adapters/openbase_adapter.rb +4 -3
  33. data/lib/active_record/connection_adapters/oracle_adapter.rb +151 -127
  34. data/lib/active_record/connection_adapters/postgresql_adapter.rb +125 -48
  35. data/lib/active_record/connection_adapters/sqlite_adapter.rb +38 -10
  36. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +183 -155
  37. data/lib/active_record/connection_adapters/sybase_adapter.rb +190 -212
  38. data/lib/active_record/deprecated_associations.rb +24 -10
  39. data/lib/active_record/deprecated_finders.rb +4 -1
  40. data/lib/active_record/fixtures.rb +37 -23
  41. data/lib/active_record/locking/optimistic.rb +106 -0
  42. data/lib/active_record/locking/pessimistic.rb +77 -0
  43. data/lib/active_record/migration.rb +8 -5
  44. data/lib/active_record/observer.rb +73 -34
  45. data/lib/active_record/reflection.rb +21 -7
  46. data/lib/active_record/schema_dumper.rb +33 -5
  47. data/lib/active_record/timestamp.rb +23 -34
  48. data/lib/active_record/transactions.rb +37 -30
  49. data/lib/active_record/validations.rb +46 -30
  50. data/lib/active_record/vendor/mysql.rb +20 -5
  51. data/lib/active_record/version.rb +2 -2
  52. data/lib/active_record/wrappings.rb +1 -2
  53. data/lib/active_record/xml_serialization.rb +308 -0
  54. data/test/aaa_create_tables_test.rb +5 -1
  55. data/test/abstract_unit.rb +18 -8
  56. data/test/{active_schema_mysql.rb → active_schema_test_mysql.rb} +2 -2
  57. data/test/adapter_test.rb +9 -7
  58. data/test/adapter_test_sqlserver.rb +81 -0
  59. data/test/aggregations_test.rb +29 -0
  60. data/test/{association_callbacks_test.rb → associations/callbacks_test.rb} +10 -8
  61. data/test/{associations_cascaded_eager_loading_test.rb → associations/cascaded_eager_loading_test.rb} +35 -3
  62. data/test/{associations_go_eager_test.rb → associations/eager_test.rb} +36 -2
  63. data/test/{associations_extensions_test.rb → associations/extension_test.rb} +5 -0
  64. data/test/{associations_join_model_test.rb → associations/join_model_test.rb} +118 -8
  65. data/test/associations_test.rb +339 -45
  66. data/test/attribute_methods_test.rb +49 -0
  67. data/test/base_test.rb +321 -67
  68. data/test/calculations_test.rb +48 -10
  69. data/test/callbacks_test.rb +13 -0
  70. data/test/connection_test_firebird.rb +8 -0
  71. data/test/connections/native_db2/connection.rb +18 -17
  72. data/test/connections/native_firebird/connection.rb +19 -17
  73. data/test/connections/native_frontbase/connection.rb +27 -0
  74. data/test/connections/native_mysql/connection.rb +18 -15
  75. data/test/connections/native_openbase/connection.rb +14 -15
  76. data/test/connections/native_oracle/connection.rb +16 -12
  77. data/test/connections/native_postgresql/connection.rb +16 -17
  78. data/test/connections/native_sqlite/connection.rb +3 -6
  79. data/test/connections/native_sqlite3/connection.rb +3 -6
  80. data/test/connections/native_sqlserver/connection.rb +16 -17
  81. data/test/connections/native_sqlserver_odbc/connection.rb +18 -19
  82. data/test/connections/native_sybase/connection.rb +16 -17
  83. data/test/datatype_test_postgresql.rb +52 -0
  84. data/test/defaults_test.rb +52 -10
  85. data/test/deprecated_associations_test.rb +151 -107
  86. data/test/deprecated_finder_test.rb +83 -66
  87. data/test/empty_date_time_test.rb +25 -0
  88. data/test/finder_test.rb +118 -11
  89. data/test/fixtures/accounts.yml +6 -1
  90. data/test/fixtures/author.rb +27 -4
  91. data/test/fixtures/categorizations.yml +8 -2
  92. data/test/fixtures/category.rb +1 -2
  93. data/test/fixtures/comments.yml +0 -6
  94. data/test/fixtures/companies.yml +6 -1
  95. data/test/fixtures/company.rb +23 -1
  96. data/test/fixtures/company_in_module.rb +8 -10
  97. data/test/fixtures/customer.rb +2 -2
  98. data/test/fixtures/customers.yml +9 -0
  99. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  100. data/test/fixtures/db_definitions/db2.sql +9 -0
  101. data/test/fixtures/db_definitions/firebird.drop.sql +3 -0
  102. data/test/fixtures/db_definitions/firebird.sql +13 -1
  103. data/test/fixtures/db_definitions/frontbase.drop.sql +31 -0
  104. data/test/fixtures/db_definitions/frontbase.sql +262 -0
  105. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  107. data/test/fixtures/db_definitions/mysql.drop.sql +1 -0
  108. data/test/fixtures/db_definitions/mysql.sql +23 -14
  109. data/test/fixtures/db_definitions/openbase.sql +13 -1
  110. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  111. data/test/fixtures/db_definitions/oracle.sql +29 -2
  112. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  113. data/test/fixtures/db_definitions/postgresql.sql +13 -3
  114. data/test/fixtures/db_definitions/schema.rb +29 -1
  115. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  116. data/test/fixtures/db_definitions/sqlite.sql +12 -3
  117. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  118. data/test/fixtures/db_definitions/sqlserver.sql +35 -0
  119. data/test/fixtures/db_definitions/sybase.drop.sql +2 -0
  120. data/test/fixtures/db_definitions/sybase.sql +13 -4
  121. data/test/fixtures/developer.rb +12 -0
  122. data/test/fixtures/edge.rb +5 -0
  123. data/test/fixtures/edges.yml +6 -0
  124. data/test/fixtures/funny_jokes.yml +3 -7
  125. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  126. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  127. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  128. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  129. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  130. data/test/fixtures/mixin.rb +15 -0
  131. data/test/fixtures/mixins.yml +38 -0
  132. data/test/fixtures/post.rb +3 -2
  133. data/test/fixtures/project.rb +3 -1
  134. data/test/fixtures/topic.rb +6 -1
  135. data/test/fixtures/topics.yml +4 -4
  136. data/test/fixtures/vertex.rb +9 -0
  137. data/test/fixtures/vertices.yml +4 -0
  138. data/test/fixtures_test.rb +45 -0
  139. data/test/inheritance_test.rb +67 -6
  140. data/test/lifecycle_test.rb +40 -19
  141. data/test/locking_test.rb +170 -26
  142. data/test/method_scoping_test.rb +2 -2
  143. data/test/migration_test.rb +387 -110
  144. data/test/migration_test_firebird.rb +124 -0
  145. data/test/mixin_nested_set_test.rb +14 -2
  146. data/test/mixin_test.rb +56 -18
  147. data/test/modules_test.rb +8 -2
  148. data/test/multiple_db_test.rb +2 -2
  149. data/test/pk_test.rb +1 -0
  150. data/test/reflection_test.rb +8 -2
  151. data/test/schema_authorization_test_postgresql.rb +75 -0
  152. data/test/schema_dumper_test.rb +40 -4
  153. data/test/table_name_test_sqlserver.rb +23 -0
  154. data/test/threaded_connections_test.rb +19 -16
  155. data/test/transactions_test.rb +86 -72
  156. data/test/validations_test.rb +126 -56
  157. data/test/xml_serialization_test.rb +125 -0
  158. metadata +45 -11
  159. data/lib/active_record/locking.rb +0 -79
@@ -0,0 +1,49 @@
1
+ require 'abstract_unit'
2
+
3
+ class AttributeMethodsTest < Test::Unit::TestCase
4
+ def setup
5
+ @old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup
6
+ @target = Class.new(ActiveRecord::Base)
7
+ @target.table_name = 'topics'
8
+ end
9
+
10
+ def teardown
11
+ ActiveRecord::Base.send(:attribute_method_suffixes).clear
12
+ ActiveRecord::Base.attribute_method_suffix *@old_suffixes
13
+ end
14
+
15
+
16
+ def test_match_attribute_method_query_returns_match_data
17
+ assert_not_nil md = @target.match_attribute_method?('title=')
18
+ assert_equal 'title', md.pre_match
19
+ assert_equal ['='], md.captures
20
+
21
+ %w(_hello_world ist! _maybe?).each do |suffix|
22
+ @target.class_eval "def attribute#{suffix}(*args) args end"
23
+ @target.attribute_method_suffix suffix
24
+
25
+ assert_not_nil md = @target.match_attribute_method?("title#{suffix}")
26
+ assert_equal 'title', md.pre_match
27
+ assert_equal [suffix], md.captures
28
+ end
29
+ end
30
+
31
+ def test_declared_attribute_method_affects_respond_to_and_method_missing
32
+ topic = @target.new(:title => 'Budget')
33
+ assert topic.respond_to?('title')
34
+ assert_equal 'Budget', topic.title
35
+ assert !topic.respond_to?('title_hello_world')
36
+ assert_raise(NoMethodError) { topic.title_hello_world }
37
+
38
+ %w(_hello_world _it! _candidate= able?).each do |suffix|
39
+ @target.class_eval "def attribute#{suffix}(*args) args end"
40
+ @target.attribute_method_suffix suffix
41
+
42
+ meth = "title#{suffix}"
43
+ assert topic.respond_to?(meth)
44
+ assert_equal ['title'], topic.send(meth)
45
+ assert_equal ['title', 'a'], topic.send(meth, 'a')
46
+ assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
47
+ end
48
+ end
49
+ end
@@ -10,10 +10,18 @@ require 'fixtures/auto_id'
10
10
  require 'fixtures/column_name'
11
11
  require 'fixtures/subscriber'
12
12
  require 'fixtures/keyboard'
13
+ require 'fixtures/post'
13
14
 
14
15
  class Category < ActiveRecord::Base; end
15
16
  class Smarts < ActiveRecord::Base; end
16
- class CreditCard < ActiveRecord::Base; end
17
+ class CreditCard < ActiveRecord::Base
18
+ class PinNumber < ActiveRecord::Base
19
+ class CvvCode < ActiveRecord::Base; end
20
+ class SubCvvCode < CvvCode; end
21
+ end
22
+ class SubPinNumber < PinNumber; end
23
+ class Brand < Category; end
24
+ end
17
25
  class MasterCreditCard < ActiveRecord::Base; end
18
26
  class Post < ActiveRecord::Base; end
19
27
  class Computer < ActiveRecord::Base; end
@@ -21,8 +29,9 @@ class NonExistentTable < ActiveRecord::Base; end
21
29
  class TestOracleDefault < ActiveRecord::Base; end
22
30
 
23
31
  class LoosePerson < ActiveRecord::Base
24
- attr_protected :credit_rating, :administrator
32
+ self.table_name = 'people'
25
33
  self.abstract_class = true
34
+ attr_protected :credit_rating, :administrator
26
35
  end
27
36
 
28
37
  class LooseDescendant < LoosePerson
@@ -30,6 +39,7 @@ class LooseDescendant < LoosePerson
30
39
  end
31
40
 
32
41
  class TightPerson < ActiveRecord::Base
42
+ self.table_name = 'people'
33
43
  attr_accessible :name, :address
34
44
  end
35
45
 
@@ -44,7 +54,7 @@ class Task < ActiveRecord::Base
44
54
  end
45
55
 
46
56
  class BasicsTest < Test::Unit::TestCase
47
- fixtures :topics, :companies, :developers, :projects, :computers
57
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts
48
58
 
49
59
  def test_table_exists
50
60
  assert !NonExistentTable.table_exists?
@@ -59,7 +69,7 @@ class BasicsTest < Test::Unit::TestCase
59
69
  assert_equal("Jason", topic.author_name)
60
70
  assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address)
61
71
  end
62
-
72
+
63
73
  def test_integers_as_nil
64
74
  test = AutoId.create('value' => '')
65
75
  assert_nil AutoId.find(test.id).value
@@ -142,8 +152,20 @@ class BasicsTest < Test::Unit::TestCase
142
152
  def test_save!
143
153
  topic = Topic.new(:title => "New Topic")
144
154
  assert topic.save!
145
- end
146
155
 
156
+ reply = Reply.new
157
+ assert_raise(ActiveRecord::RecordInvalid) { reply.save! }
158
+ end
159
+
160
+ def test_save_null_string_attributes
161
+ topic = Topic.find(1)
162
+ topic.attributes = { "title" => "null", "author_name" => "null" }
163
+ topic.save!
164
+ topic.reload
165
+ assert_equal("null", topic.title)
166
+ assert_equal("null", topic.author_name)
167
+ end
168
+
147
169
  def test_hashes_not_mangled
148
170
  new_topic = { :title => "New Topic" }
149
171
  new_topic_values = { :title => "AnotherTopic" }
@@ -315,23 +337,22 @@ class BasicsTest < Test::Unit::TestCase
315
337
  Time, Topic.find(1).written_on,
316
338
  "The written_on attribute should be of the Time class"
317
339
  )
340
+
341
+ # For adapters which support microsecond resolution.
342
+ if current_adapter?(:PostgreSQLAdapter)
343
+ assert_equal 11, Topic.find(1).written_on.sec
344
+ assert_equal 223300, Topic.find(1).written_on.usec
345
+ assert_equal 9900, Topic.find(2).written_on.usec
346
+ end
318
347
  end
319
-
348
+
320
349
  def test_destroy
321
- topic = Topic.new
322
- topic.title = "Yet Another New Topic"
323
- topic.written_on = "2003-12-12 23:23:00"
324
- topic.save
325
- topic.destroy
350
+ topic = Topic.find(1)
351
+ assert_equal topic, topic.destroy, 'topic.destroy did not return self'
352
+ assert topic.frozen?, 'topic not frozen after destroy'
326
353
  assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
327
354
  end
328
-
329
- def test_destroy_returns_self
330
- topic = Topic.new("title" => "Yet Another Title")
331
- assert topic.save
332
- assert_equal topic, topic.destroy, "destroy did not return destroyed object"
333
- end
334
-
355
+
335
356
  def test_record_not_found_exception
336
357
  assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
337
358
  end
@@ -368,21 +389,33 @@ class BasicsTest < Test::Unit::TestCase
368
389
  end
369
390
 
370
391
  def test_table_name_guesses
392
+ classes = [Category, Smarts, CreditCard, CreditCard::PinNumber, CreditCard::PinNumber::CvvCode, CreditCard::SubPinNumber, CreditCard::Brand, MasterCreditCard]
393
+
371
394
  assert_equal "topics", Topic.table_name
372
-
395
+
373
396
  assert_equal "categories", Category.table_name
374
397
  assert_equal "smarts", Smarts.table_name
375
398
  assert_equal "credit_cards", CreditCard.table_name
399
+ assert_equal "credit_card_pin_numbers", CreditCard::PinNumber.table_name
400
+ assert_equal "credit_card_pin_number_cvv_codes", CreditCard::PinNumber::CvvCode.table_name
401
+ assert_equal "credit_card_pin_numbers", CreditCard::SubPinNumber.table_name
402
+ assert_equal "categories", CreditCard::Brand.table_name
376
403
  assert_equal "master_credit_cards", MasterCreditCard.table_name
377
404
 
378
405
  ActiveRecord::Base.pluralize_table_names = false
379
- [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name}
406
+ classes.each(&:reset_table_name)
407
+
380
408
  assert_equal "category", Category.table_name
381
409
  assert_equal "smarts", Smarts.table_name
382
410
  assert_equal "credit_card", CreditCard.table_name
411
+ assert_equal "credit_card_pin_number", CreditCard::PinNumber.table_name
412
+ assert_equal "credit_card_pin_number_cvv_code", CreditCard::PinNumber::CvvCode.table_name
413
+ assert_equal "credit_card_pin_number", CreditCard::SubPinNumber.table_name
414
+ assert_equal "category", CreditCard::Brand.table_name
383
415
  assert_equal "master_credit_card", MasterCreditCard.table_name
416
+
384
417
  ActiveRecord::Base.pluralize_table_names = true
385
- [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name}
418
+ classes.each(&:reset_table_name)
386
419
 
387
420
  ActiveRecord::Base.table_name_prefix = "test_"
388
421
  Category.reset_table_name
@@ -410,8 +443,9 @@ class BasicsTest < Test::Unit::TestCase
410
443
  ActiveRecord::Base.table_name_suffix = ""
411
444
  Category.reset_table_name
412
445
  assert_equal "category", Category.table_name
446
+
413
447
  ActiveRecord::Base.pluralize_table_names = true
414
- [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name}
448
+ classes.each(&:reset_table_name)
415
449
  end
416
450
 
417
451
  def test_destroy_all
@@ -439,18 +473,18 @@ class BasicsTest < Test::Unit::TestCase
439
473
 
440
474
  def test_increment_counter
441
475
  Topic.increment_counter("replies_count", 1)
442
- assert_equal 1, Topic.find(1).replies_count
476
+ assert_equal 2, Topic.find(1).replies_count
443
477
 
444
478
  Topic.increment_counter("replies_count", 1)
445
- assert_equal 2, Topic.find(1).replies_count
479
+ assert_equal 3, Topic.find(1).replies_count
446
480
  end
447
481
 
448
482
  def test_decrement_counter
449
483
  Topic.decrement_counter("replies_count", 2)
450
- assert_equal 1, Topic.find(2).replies_count
484
+ assert_equal -1, Topic.find(2).replies_count
451
485
 
452
486
  Topic.decrement_counter("replies_count", 2)
453
- assert_equal 0, Topic.find(1).replies_count
487
+ assert_equal -2, Topic.find(2).replies_count
454
488
  end
455
489
 
456
490
  def test_update_all
@@ -549,16 +583,29 @@ class BasicsTest < Test::Unit::TestCase
549
583
  end
550
584
  end
551
585
 
552
- def test_utc_as_time_zone
553
- # Oracle and SQLServer do not have a TIME datatype.
554
- return true if current_adapter?(:SQLServerAdapter) || current_adapter?(:OracleAdapter)
586
+ # Oracle, SQLServer, and Sybase do not have a TIME datatype.
587
+ unless current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter)
588
+ def test_utc_as_time_zone
589
+ Topic.default_timezone = :utc
590
+ attributes = { "bonus_time" => "5:42:00AM" }
591
+ topic = Topic.find(1)
592
+ topic.attributes = attributes
593
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
594
+ Topic.default_timezone = :local
595
+ end
555
596
 
556
- Topic.default_timezone = :utc
557
- attributes = { "bonus_time" => "5:42:00AM" }
558
- topic = Topic.find(1)
559
- topic.attributes = attributes
560
- assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
561
- Topic.default_timezone = :local
597
+ def test_utc_as_time_zone_and_new
598
+ Topic.default_timezone = :utc
599
+ attributes = { "bonus_time(1i)"=>"2000",
600
+ "bonus_time(2i)"=>"1",
601
+ "bonus_time(3i)"=>"1",
602
+ "bonus_time(4i)"=>"10",
603
+ "bonus_time(5i)"=>"35",
604
+ "bonus_time(6i)"=>"50" }
605
+ topic = Topic.new(attributes)
606
+ assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
607
+ Topic.default_timezone = :local
608
+ end
562
609
  end
563
610
 
564
611
  def test_default_values_on_empty_strings
@@ -614,6 +661,40 @@ class BasicsTest < Test::Unit::TestCase
614
661
  assert !Topic.find(1).approved?
615
662
  end
616
663
 
664
+ def test_update_attributes
665
+ topic = Topic.find(1)
666
+ assert !topic.approved?
667
+ assert_equal "The First Topic", topic.title
668
+
669
+ topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
670
+ topic.reload
671
+ assert topic.approved?
672
+ assert_equal "The First Topic Updated", topic.title
673
+
674
+ topic.update_attributes(:approved => false, :title => "The First Topic")
675
+ topic.reload
676
+ assert !topic.approved?
677
+ assert_equal "The First Topic", topic.title
678
+ end
679
+
680
+ def test_update_attributes!
681
+ reply = Reply.find(2)
682
+ assert_equal "The Second Topic's of the day", reply.title
683
+ assert_equal "Have a nice day", reply.content
684
+
685
+ reply.update_attributes!("title" => "The Second Topic's of the day updated", "content" => "Have a nice evening")
686
+ reply.reload
687
+ assert_equal "The Second Topic's of the day updated", reply.title
688
+ assert_equal "Have a nice evening", reply.content
689
+
690
+ reply.update_attributes!(:title => "The Second Topic's of the day", :content => "Have a nice day")
691
+ reply.reload
692
+ assert_equal "The Second Topic's of the day", reply.title
693
+ assert_equal "Have a nice day", reply.content
694
+
695
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") }
696
+ end
697
+
617
698
  def test_mass_assignment_protection
618
699
  firm = Firm.new
619
700
  firm.attributes = { "name" => "Next Angle", "rating" => 5 }
@@ -732,8 +813,8 @@ class BasicsTest < Test::Unit::TestCase
732
813
  end
733
814
 
734
815
  def test_attributes_on_dummy_time
735
- # Oracle and SQL Server do not have a TIME datatype.
736
- return true if current_adapter?(:SQLServerAdapter) || current_adapter?(:OracleAdapter)
816
+ # Oracle, SQL Server, and Sybase do not have a TIME datatype.
817
+ return true if current_adapter?(:SQLServerAdapter, :OracleAdapter, :SybaseAdapter)
737
818
 
738
819
  attributes = {
739
820
  "bonus_time" => "5:42:00AM"
@@ -903,12 +984,44 @@ class BasicsTest < Test::Unit::TestCase
903
984
  end
904
985
  end
905
986
 
987
+ class NumericData < ActiveRecord::Base
988
+ self.table_name = 'numeric_data'
989
+ end
990
+
991
+ def test_numeric_fields
992
+ m = NumericData.new(
993
+ :bank_balance => 1586.43,
994
+ :big_bank_balance => BigDecimal("1000234000567.95"),
995
+ :world_population => 6000000000,
996
+ :my_house_population => 3
997
+ )
998
+ assert m.save
999
+
1000
+ m1 = NumericData.find(m.id)
1001
+ assert_not_nil m1
1002
+
1003
+ # As with migration_test.rb, we should make world_population >= 2**62
1004
+ # to cover 64-bit platforms and test it is a Bignum, but the main thing
1005
+ # is that it's an Integer.
1006
+ assert_kind_of Integer, m1.world_population
1007
+ assert_equal 6000000000, m1.world_population
1008
+
1009
+ assert_kind_of Fixnum, m1.my_house_population
1010
+ assert_equal 3, m1.my_house_population
1011
+
1012
+ assert_kind_of BigDecimal, m1.bank_balance
1013
+ assert_equal BigDecimal("1586.43"), m1.bank_balance
1014
+
1015
+ assert_kind_of BigDecimal, m1.big_bank_balance
1016
+ assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance
1017
+ end
1018
+
906
1019
  def test_auto_id
907
1020
  auto = AutoId.new
908
1021
  auto.save
909
1022
  assert (auto.id > 0)
910
1023
  end
911
-
1024
+
912
1025
  def quote_column_name(name)
913
1026
  "<#{name}>"
914
1027
  end
@@ -923,13 +1036,9 @@ class BasicsTest < Test::Unit::TestCase
923
1036
  end
924
1037
 
925
1038
  def test_sql_injection_via_find
926
- assert_raises(ActiveRecord::RecordNotFound) do
1039
+ assert_raises(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
927
1040
  Topic.find("123456 OR id > 0")
928
1041
  end
929
-
930
- assert_raises(ActiveRecord::RecordNotFound) do
931
- Topic.find(";;; this should raise an RecordNotFound error")
932
- end
933
1042
  end
934
1043
 
935
1044
  def test_column_name_properly_quoted
@@ -942,6 +1051,14 @@ class BasicsTest < Test::Unit::TestCase
942
1051
  assert_equal(41, c2.references)
943
1052
  end
944
1053
 
1054
+ def test_quoting_arrays
1055
+ replies = Reply.find(:all, :conditions => [ "id IN (?)", topics(:first).replies.collect(&:id) ])
1056
+ assert_equal topics(:first).replies.size, replies.size
1057
+
1058
+ replies = Reply.find(:all, :conditions => [ "id IN (?)", [] ])
1059
+ assert_equal 0, replies.size
1060
+ end
1061
+
945
1062
  MyObject = Struct.new :attribute1, :attribute2
946
1063
 
947
1064
  def test_serialized_attribute
@@ -970,6 +1087,18 @@ class BasicsTest < Test::Unit::TestCase
970
1087
  assert_equal author_name, Topic.find(topic.id).author_name
971
1088
  end
972
1089
 
1090
+ def test_quote_chars
1091
+ str = 'The Narrator'
1092
+ topic = Topic.create(:author_name => str)
1093
+ assert_equal str, topic.author_name
1094
+
1095
+ assert_kind_of ActiveSupport::Multibyte::Chars, str.chars
1096
+ topic = Topic.find_by_author_name(str.chars)
1097
+
1098
+ assert_kind_of Topic, topic
1099
+ assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars"
1100
+ end
1101
+
973
1102
  def test_class_level_destroy
974
1103
  should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
975
1104
  Topic.find(1).replies << should_be_destroyed_reply
@@ -989,12 +1118,12 @@ class BasicsTest < Test::Unit::TestCase
989
1118
  end
990
1119
 
991
1120
  def test_increment_attribute
992
- assert_equal 0, topics(:first).replies_count
1121
+ assert_equal 1, topics(:first).replies_count
993
1122
  topics(:first).increment! :replies_count
994
- assert_equal 1, topics(:first, :reload).replies_count
1123
+ assert_equal 2, topics(:first, :reload).replies_count
995
1124
 
996
1125
  topics(:first).increment(:replies_count).increment!(:replies_count)
997
- assert_equal 3, topics(:first, :reload).replies_count
1126
+ assert_equal 4, topics(:first, :reload).replies_count
998
1127
  end
999
1128
 
1000
1129
  def test_increment_nil_attribute
@@ -1005,13 +1134,13 @@ class BasicsTest < Test::Unit::TestCase
1005
1134
 
1006
1135
  def test_decrement_attribute
1007
1136
  topics(:first).increment(:replies_count).increment!(:replies_count)
1008
- assert_equal 2, topics(:first).replies_count
1137
+ assert_equal 3, topics(:first).replies_count
1009
1138
 
1010
1139
  topics(:first).decrement!(:replies_count)
1011
- assert_equal 1, topics(:first, :reload).replies_count
1140
+ assert_equal 2, topics(:first, :reload).replies_count
1012
1141
 
1013
1142
  topics(:first).decrement(:replies_count).decrement!(:replies_count)
1014
- assert_equal -1, topics(:first, :reload).replies_count
1143
+ assert_equal 0, topics(:first, :reload).replies_count
1015
1144
  end
1016
1145
 
1017
1146
  def test_toggle_attribute
@@ -1091,7 +1220,7 @@ class BasicsTest < Test::Unit::TestCase
1091
1220
  def test_count_with_join
1092
1221
  res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'"
1093
1222
  res2 = nil
1094
- assert_nothing_raised do
1223
+ assert_deprecated 'count' do
1095
1224
  res2 = Post.count("posts.#{QUOTED_TYPE} = 'Post'",
1096
1225
  "LEFT JOIN comments ON posts.id=comments.post_id")
1097
1226
  end
@@ -1104,21 +1233,21 @@ class BasicsTest < Test::Unit::TestCase
1104
1233
  end
1105
1234
  assert_equal res, res3
1106
1235
 
1107
- res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id"
1236
+ res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
1108
1237
  res5 = nil
1109
1238
  assert_nothing_raised do
1110
- res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id",
1111
- :joins => "p, comments c",
1239
+ res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
1240
+ :joins => "p, comments co",
1112
1241
  :select => "p.id")
1113
1242
  end
1114
1243
 
1115
1244
  assert_equal res4, res5
1116
1245
 
1117
- res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id"
1246
+ res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
1118
1247
  res7 = nil
1119
1248
  assert_nothing_raised do
1120
- res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id",
1121
- :joins => "p, comments c",
1249
+ res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
1250
+ :joins => "p, comments co",
1122
1251
  :select => "p.id",
1123
1252
  :distinct => true)
1124
1253
  end
@@ -1174,13 +1303,104 @@ class BasicsTest < Test::Unit::TestCase
1174
1303
  assert_equal Developer.count, developers.size
1175
1304
  end
1176
1305
 
1177
- def test_base_class
1306
+ def test_scoped_find_order
1307
+ # Test order in scope
1308
+ scoped_developers = Developer.with_scope(:find => { :limit => 1, :order => 'salary DESC' }) do
1309
+ Developer.find(:all)
1310
+ end
1311
+ assert_equal 'Jamis', scoped_developers.first.name
1312
+ assert scoped_developers.include?(developers(:jamis))
1313
+ # Test scope without order and order in find
1314
+ scoped_developers = Developer.with_scope(:find => { :limit => 1 }) do
1315
+ Developer.find(:all, :order => 'salary DESC')
1316
+ end
1317
+ # Test scope order + find order, find has priority
1318
+ scoped_developers = Developer.with_scope(:find => { :limit => 3, :order => 'id DESC' }) do
1319
+ Developer.find(:all, :order => 'salary ASC')
1320
+ end
1321
+ assert scoped_developers.include?(developers(:poor_jamis))
1322
+ assert scoped_developers.include?(developers(:david))
1323
+ assert scoped_developers.include?(developers(:dev_10))
1324
+ # Test without scoped find conditions to ensure we get the right thing
1325
+ developers = Developer.find(:all, :order => 'id', :limit => 1)
1326
+ assert scoped_developers.include?(developers(:david))
1327
+ end
1328
+
1329
+ def test_scoped_find_limit_offset_including_has_many_association
1330
+ topics = Topic.with_scope(:find => {:limit => 1, :offset => 1, :include => :replies}) do
1331
+ Topic.find(:all, :order => "topics.id")
1332
+ end
1333
+ assert_equal 1, topics.size
1334
+ assert_equal 2, topics.first.id
1335
+ end
1336
+
1337
+ def test_scoped_find_order_including_has_many_association
1338
+ developers = Developer.with_scope(:find => { :order => 'developers.salary DESC', :include => :projects }) do
1339
+ Developer.find(:all)
1340
+ end
1341
+ assert developers.size >= 2
1342
+ for i in 1...developers.size
1343
+ assert developers[i-1].salary >= developers[i].salary
1344
+ end
1345
+ end
1346
+
1347
+ def test_abstract_class
1348
+ assert !ActiveRecord::Base.abstract_class?
1178
1349
  assert LoosePerson.abstract_class?
1179
1350
  assert !LooseDescendant.abstract_class?
1351
+ end
1352
+
1353
+ def test_base_class
1180
1354
  assert_equal LoosePerson, LoosePerson.base_class
1181
1355
  assert_equal LooseDescendant, LooseDescendant.base_class
1182
1356
  assert_equal TightPerson, TightPerson.base_class
1183
1357
  assert_equal TightPerson, TightDescendant.base_class
1358
+
1359
+ assert_equal Post, Post.base_class
1360
+ assert_equal Post, SpecialPost.base_class
1361
+ assert_equal Post, StiPost.base_class
1362
+ assert_equal SubStiPost, SubStiPost.base_class
1363
+ end
1364
+
1365
+ def test_descends_from_active_record
1366
+ # Tries to call Object.abstract_class?
1367
+ assert_raise(NoMethodError) do
1368
+ ActiveRecord::Base.descends_from_active_record?
1369
+ end
1370
+
1371
+ # Abstract subclass of AR::Base.
1372
+ assert LoosePerson.descends_from_active_record?
1373
+
1374
+ # Concrete subclass of an abstract class.
1375
+ assert LooseDescendant.descends_from_active_record?
1376
+
1377
+ # Concrete subclass of AR::Base.
1378
+ assert TightPerson.descends_from_active_record?
1379
+
1380
+ # Concrete subclass of a concrete class but has no type column.
1381
+ assert TightDescendant.descends_from_active_record?
1382
+
1383
+ # Concrete subclass of AR::Base.
1384
+ assert Post.descends_from_active_record?
1385
+
1386
+ # Abstract subclass of a concrete class which has a type column.
1387
+ # This is pathological, as you'll never have Sub < Abstract < Concrete.
1388
+ assert !StiPost.descends_from_active_record?
1389
+
1390
+ # Concrete subclasses an abstract class which has a type column.
1391
+ assert !SubStiPost.descends_from_active_record?
1392
+ end
1393
+
1394
+ def test_find_on_abstract_base_class_doesnt_use_type_condition
1395
+ old_class = LooseDescendant
1396
+ Object.send :remove_const, :LooseDescendant
1397
+
1398
+ descendant = old_class.create!
1399
+ assert_not_nil LoosePerson.find(descendant.id), "Should have found instance of LooseDescendant when finding abstract LoosePerson: #{descendant.inspect}"
1400
+ ensure
1401
+ unless Object.const_defined?(:LooseDescendant)
1402
+ Object.const_set :LooseDescendant, old_class
1403
+ end
1184
1404
  end
1185
1405
 
1186
1406
  def test_assert_queries
@@ -1199,41 +1419,65 @@ class BasicsTest < Test::Unit::TestCase
1199
1419
  assert xml.include?(%(<title>The First Topic</title>))
1200
1420
  assert xml.include?(%(<author-name>David</author-name>))
1201
1421
  assert xml.include?(%(<id type="integer">1</id>))
1202
- assert xml.include?(%(<replies-count type="integer">0</replies-count>))
1422
+ assert xml.include?(%(<replies-count type="integer">1</replies-count>))
1203
1423
  assert xml.include?(%(<written-on type="datetime">#{written_on_in_current_timezone}</written-on>))
1204
1424
  assert xml.include?(%(<content>Have a nice day</content>))
1205
1425
  assert xml.include?(%(<author-email-address>david@loudthinking.com</author-email-address>))
1206
- assert xml.include?(%(<parent-id></parent-id>))
1207
- if current_adapter?(:SybaseAdapter) or current_adapter?(:SQLServerAdapter)
1426
+ assert xml.match(%(<parent-id type="integer"></parent-id>))
1427
+ if current_adapter?(:SybaseAdapter, :SQLServerAdapter, :OracleAdapter)
1208
1428
  assert xml.include?(%(<last-read type="datetime">#{last_read_in_current_timezone}</last-read>))
1209
1429
  else
1210
1430
  assert xml.include?(%(<last-read type="date">2004-04-15</last-read>))
1211
1431
  end
1212
1432
  # Oracle and DB2 don't have true boolean or time-only fields
1213
- unless current_adapter?(:OracleAdapter) || current_adapter?(:DB2Adapter)
1433
+ unless current_adapter?(:OracleAdapter, :DB2Adapter)
1214
1434
  assert xml.include?(%(<approved type="boolean">false</approved>)), "Approved should be a boolean"
1215
1435
  assert xml.include?(%(<bonus-time type="datetime">#{bonus_time_in_current_timezone}</bonus-time>))
1216
1436
  end
1217
1437
  end
1218
-
1438
+
1219
1439
  def test_to_xml_skipping_attributes
1220
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => :title)
1440
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
1221
1441
  assert_equal "<topic>", xml.first(7)
1222
1442
  assert !xml.include?(%(<title>The First Topic</title>))
1223
1443
  assert xml.include?(%(<author-name>David</author-name>))
1224
1444
 
1225
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [ :title, :author_name ])
1445
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
1226
1446
  assert !xml.include?(%(<title>The First Topic</title>))
1227
1447
  assert !xml.include?(%(<author-name>David</author-name>))
1228
1448
  end
1229
-
1449
+
1230
1450
  def test_to_xml_including_has_many_association
1231
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
1451
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
1232
1452
  assert_equal "<topic>", xml.first(7)
1233
1453
  assert xml.include?(%(<replies><reply>))
1234
1454
  assert xml.include?(%(<title>The Second Topic's of the day</title>))
1235
1455
  end
1236
1456
 
1457
+ def test_array_to_xml_including_has_many_association
1458
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
1459
+ assert xml.include?(%(<replies><reply>))
1460
+ end
1461
+
1462
+ def test_array_to_xml_including_methods
1463
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
1464
+ assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
1465
+ assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
1466
+ end
1467
+
1468
+ def test_array_to_xml_including_has_one_association
1469
+ xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
1470
+ assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
1471
+ assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
1472
+ end
1473
+
1474
+ def test_array_to_xml_including_belongs_to_association
1475
+ xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
1476
+ assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
1477
+ assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
1478
+ assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
1479
+ end
1480
+
1237
1481
  def test_to_xml_including_belongs_to_association
1238
1482
  xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
1239
1483
  assert !xml.include?("<firm>")
@@ -1260,6 +1504,12 @@ class BasicsTest < Test::Unit::TestCase
1260
1504
  assert xml.include?(%(<clients><client>))
1261
1505
  end
1262
1506
 
1507
+ def test_to_xml_including_methods
1508
+ xml = Company.new.to_xml(:methods => :arbitrary_method, :skip_instruct => true)
1509
+ assert_equal "<company>", xml.first(9)
1510
+ assert xml.include?(%(<arbitrary-method>I am Jack's profound disappointment</arbitrary-method>))
1511
+ end
1512
+
1263
1513
  def test_except_attributes
1264
1514
  assert_equal(
1265
1515
  %w( author_name type id approved replies_count bonus_time written_on content author_email_address parent_id last_read),
@@ -1281,6 +1531,10 @@ class BasicsTest < Test::Unit::TestCase
1281
1531
  assert_equal 'ActiveRecord::Person', ActiveRecord::Base.send(:type_name_with_module, 'Person')
1282
1532
  assert_equal '::Person', ActiveRecord::Base.send(:type_name_with_module, '::Person')
1283
1533
  end
1534
+
1535
+ def test_to_param_should_return_string
1536
+ assert_kind_of String, Client.find(:first).to_param
1537
+ end
1284
1538
 
1285
1539
  # FIXME: this test ought to run, but it needs to run sandboxed so that it
1286
1540
  # doesn't b0rk the current test environment by undefing everything.
@@ -1306,7 +1560,7 @@ class BasicsTest < Test::Unit::TestCase
1306
1560
 
1307
1561
  private
1308
1562
  def assert_readers(model, exceptions)
1309
- expected_readers = Set.new(model.column_names - (model.serialized_attributes.keys + ['id']))
1563
+ expected_readers = Set.new(model.column_names - ['id'])
1310
1564
  expected_readers += expected_readers.map { |col| "#{col}?" }
1311
1565
  expected_readers -= exceptions
1312
1566
  assert_equal expected_readers, model.read_methods