activerecord 2.1.2 → 2.2.2

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 (110) hide show
  1. data/CHANGELOG +32 -6
  2. data/README +0 -0
  3. data/Rakefile +4 -5
  4. data/lib/active_record.rb +11 -10
  5. data/lib/active_record/aggregations.rb +110 -38
  6. data/lib/active_record/association_preload.rb +104 -15
  7. data/lib/active_record/associations.rb +427 -212
  8. data/lib/active_record/associations/association_collection.rb +101 -16
  9. data/lib/active_record/associations/association_proxy.rb +65 -13
  10. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
  13. data/lib/active_record/associations/has_many_association.rb +28 -28
  14. data/lib/active_record/associations/has_many_through_association.rb +21 -19
  15. data/lib/active_record/associations/has_one_association.rb +24 -7
  16. data/lib/active_record/associations/has_one_through_association.rb +3 -4
  17. data/lib/active_record/attribute_methods.rb +13 -5
  18. data/lib/active_record/base.rb +435 -212
  19. data/lib/active_record/calculations.rb +12 -5
  20. data/lib/active_record/callbacks.rb +28 -9
  21. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
  28. data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
  29. data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
  30. data/lib/active_record/dirty.rb +25 -7
  31. data/lib/active_record/dynamic_finder_match.rb +41 -0
  32. data/lib/active_record/fixtures.rb +10 -9
  33. data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
  34. data/lib/active_record/locale/en.yml +54 -0
  35. data/lib/active_record/migration.rb +47 -10
  36. data/lib/active_record/named_scope.rb +29 -16
  37. data/lib/active_record/reflection.rb +118 -54
  38. data/lib/active_record/schema_dumper.rb +13 -7
  39. data/lib/active_record/test_case.rb +18 -5
  40. data/lib/active_record/transactions.rb +89 -34
  41. data/lib/active_record/validations.rb +270 -180
  42. data/lib/active_record/version.rb +1 -1
  43. data/test/cases/active_schema_test_mysql.rb +5 -0
  44. data/test/cases/adapter_test.rb +6 -0
  45. data/test/cases/aggregations_test.rb +39 -0
  46. data/test/cases/associations/belongs_to_associations_test.rb +10 -0
  47. data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
  48. data/test/cases/associations/eager_test.rb +54 -5
  49. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
  50. data/test/cases/associations/has_many_associations_test.rb +74 -7
  51. data/test/cases/associations/has_many_through_associations_test.rb +50 -3
  52. data/test/cases/associations/has_one_associations_test.rb +17 -0
  53. data/test/cases/associations/has_one_through_associations_test.rb +49 -1
  54. data/test/cases/associations_test.rb +0 -0
  55. data/test/cases/attribute_methods_test.rb +59 -4
  56. data/test/cases/base_test.rb +93 -21
  57. data/test/cases/binary_test.rb +1 -5
  58. data/test/cases/calculations_test.rb +5 -0
  59. data/test/cases/callbacks_observers_test.rb +38 -0
  60. data/test/cases/connection_test_mysql.rb +1 -1
  61. data/test/cases/defaults_test.rb +32 -1
  62. data/test/cases/deprecated_finder_test.rb +0 -0
  63. data/test/cases/dirty_test.rb +13 -0
  64. data/test/cases/finder_test.rb +162 -12
  65. data/test/cases/fixtures_test.rb +32 -3
  66. data/test/cases/helper.rb +15 -0
  67. data/test/cases/i18n_test.rb +41 -0
  68. data/test/cases/inheritance_test.rb +2 -2
  69. data/test/cases/lifecycle_test.rb +0 -0
  70. data/test/cases/locking_test.rb +4 -9
  71. data/test/cases/method_scoping_test.rb +109 -2
  72. data/test/cases/migration_test.rb +43 -8
  73. data/test/cases/multiple_db_test.rb +25 -0
  74. data/test/cases/named_scope_test.rb +74 -0
  75. data/test/cases/pooled_connections_test.rb +103 -0
  76. data/test/cases/readonly_test.rb +0 -0
  77. data/test/cases/reflection_test.rb +11 -3
  78. data/test/cases/reload_models_test.rb +20 -0
  79. data/test/cases/sanitize_test.rb +25 -0
  80. data/test/cases/schema_authorization_test_postgresql.rb +2 -2
  81. data/test/cases/transactions_test.rb +62 -12
  82. data/test/cases/unconnected_test.rb +0 -0
  83. data/test/cases/validations_i18n_test.rb +921 -0
  84. data/test/cases/validations_test.rb +44 -33
  85. data/test/connections/native_mysql/connection.rb +1 -3
  86. data/test/fixtures/companies.yml +1 -0
  87. data/test/fixtures/customers.yml +10 -1
  88. data/test/fixtures/fixture_database.sqlite3 +0 -0
  89. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  90. data/test/fixtures/organizations.yml +5 -0
  91. data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
  92. data/test/models/author.rb +3 -0
  93. data/test/models/category.rb +3 -0
  94. data/test/models/club.rb +6 -0
  95. data/test/models/company.rb +25 -1
  96. data/test/models/customer.rb +19 -1
  97. data/test/models/member.rb +2 -0
  98. data/test/models/member_detail.rb +4 -0
  99. data/test/models/organization.rb +4 -0
  100. data/test/models/parrot.rb +1 -0
  101. data/test/models/post.rb +3 -0
  102. data/test/models/reply.rb +0 -0
  103. data/test/models/topic.rb +3 -0
  104. data/test/schema/schema.rb +12 -1
  105. metadata +22 -10
  106. data/lib/active_record/vendor/mysql.rb +0 -1214
  107. data/test/cases/adapter_test_sqlserver.rb +0 -95
  108. data/test/cases/table_name_test_sqlserver.rb +0 -23
  109. data/test/cases/threaded_connections_test.rb +0 -48
  110. data/test/schema/sqlserver_specific_schema.rb +0 -5
@@ -34,8 +34,10 @@ module ActiveRecord
34
34
  # person.name << 'by'
35
35
  # person.name_change # => ['uncle bob', 'uncle bobby']
36
36
  module Dirty
37
+ DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
38
+
37
39
  def self.included(base)
38
- base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
40
+ base.attribute_method_suffix *DIRTY_SUFFIXES
39
41
  base.alias_method_chain :write_attribute, :dirty
40
42
  base.alias_method_chain :save, :dirty
41
43
  base.alias_method_chain :save!, :dirty
@@ -44,6 +46,8 @@ module ActiveRecord
44
46
 
45
47
  base.superclass_delegating_accessor :partial_updates
46
48
  base.partial_updates = true
49
+
50
+ base.send(:extend, ClassMethods)
47
51
  end
48
52
 
49
53
  # Do any attributes have unsaved changes?
@@ -62,7 +66,7 @@ module ActiveRecord
62
66
  changed_attributes.keys
63
67
  end
64
68
 
65
- # Map of changed attrs => [original value, new value]
69
+ # Map of changed attrs => [original value, new value].
66
70
  # person.changes # => {}
67
71
  # person.name = 'bob'
68
72
  # person.changes # => { 'name' => ['bill', 'bob'] }
@@ -93,27 +97,27 @@ module ActiveRecord
93
97
  end
94
98
 
95
99
  private
96
- # Map of change attr => original value.
100
+ # Map of change <tt>attr => original value</tt>.
97
101
  def changed_attributes
98
102
  @changed_attributes ||= {}
99
103
  end
100
104
 
101
- # Handle *_changed? for method_missing.
105
+ # Handle <tt>*_changed?</tt> for +method_missing+.
102
106
  def attribute_changed?(attr)
103
107
  changed_attributes.include?(attr)
104
108
  end
105
109
 
106
- # Handle *_change for method_missing.
110
+ # Handle <tt>*_change</tt> for +method_missing+.
107
111
  def attribute_change(attr)
108
112
  [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
109
113
  end
110
114
 
111
- # Handle *_was for method_missing.
115
+ # Handle <tt>*_was</tt> for +method_missing+.
112
116
  def attribute_was(attr)
113
117
  attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
114
118
  end
115
119
 
116
- # Handle *_will_change! for method_missing.
120
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
117
121
  def attribute_will_change!(attr)
118
122
  changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
119
123
  end
@@ -161,5 +165,19 @@ module ActiveRecord
161
165
  old != value
162
166
  end
163
167
 
168
+ module ClassMethods
169
+ def self.extended(base)
170
+ base.metaclass.alias_method_chain(:alias_attribute, :dirty)
171
+ end
172
+
173
+ def alias_attribute_with_dirty(new_name, old_name)
174
+ alias_attribute_without_dirty(new_name, old_name)
175
+ DIRTY_SUFFIXES.each do |suffix|
176
+ module_eval <<-STR, __FILE__, __LINE__+1
177
+ def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
178
+ STR
179
+ end
180
+ end
181
+ end
164
182
  end
165
183
  end
@@ -0,0 +1,41 @@
1
+ module ActiveRecord
2
+ class DynamicFinderMatch
3
+ def self.match(method)
4
+ df_match = self.new(method)
5
+ df_match.finder ? df_match : nil
6
+ end
7
+
8
+ def initialize(method)
9
+ @finder = :first
10
+ case method.to_s
11
+ when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
12
+ @finder = :last if $1 == 'last_by'
13
+ @finder = :all if $1 == 'all_by'
14
+ names = $2
15
+ when /^find_by_([_a-zA-Z]\w*)\!$/
16
+ @bang = true
17
+ names = $1
18
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
19
+ @instantiator = $1 == 'initialize' ? :new : :create
20
+ names = $2
21
+ else
22
+ @finder = nil
23
+ end
24
+ @attribute_names = names && names.split('_and_')
25
+ end
26
+
27
+ attr_reader :finder, :attribute_names, :instantiator
28
+
29
+ def finder?
30
+ !@finder.nil? && @instantiator.nil?
31
+ end
32
+
33
+ def instantiator?
34
+ @finder == :first && !@instantiator.nil?
35
+ end
36
+
37
+ def bang?
38
+ @bang
39
+ end
40
+ end
41
+ end
@@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
515
515
 
516
516
  all_loaded_fixtures.update(fixtures_map)
517
517
 
518
- connection.transaction(Thread.current['open_transactions'].to_i == 0) do
518
+ connection.transaction(connection.open_transactions.zero?) do
519
519
  fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
520
520
  fixtures.each { |fixture| fixture.insert_fixtures }
521
521
 
@@ -541,13 +541,14 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
541
541
  label.to_s.hash.abs
542
542
  end
543
543
 
544
- attr_reader :table_name
544
+ attr_reader :table_name, :name
545
545
 
546
546
  def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
547
547
  @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
548
+ @name = table_name # preserve fixture base name
548
549
  @class_name = class_name ||
549
550
  (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
550
- @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
551
+ @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
551
552
  @table_name = class_name.table_name if class_name.respond_to?(:table_name)
552
553
  @connection = class_name.connection if class_name.respond_to?(:connection)
553
554
  read_fixture_files
@@ -929,7 +930,7 @@ module Test #:nodoc:
929
930
  load_fixtures
930
931
  @@already_loaded_fixtures[self.class] = @loaded_fixtures
931
932
  end
932
- ActiveRecord::Base.send :increment_open_transactions
933
+ ActiveRecord::Base.connection.increment_open_transactions
933
934
  ActiveRecord::Base.connection.begin_db_transaction
934
935
  # Load fixtures for every test.
935
936
  else
@@ -950,11 +951,11 @@ module Test #:nodoc:
950
951
  end
951
952
 
952
953
  # Rollback changes if a transaction is active.
953
- if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
954
+ if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
954
955
  ActiveRecord::Base.connection.rollback_db_transaction
955
- Thread.current['open_transactions'] = 0
956
+ ActiveRecord::Base.connection.decrement_open_transactions
956
957
  end
957
- ActiveRecord::Base.verify_active_connections!
958
+ ActiveRecord::Base.clear_active_connections!
958
959
  end
959
960
 
960
961
  private
@@ -963,9 +964,9 @@ module Test #:nodoc:
963
964
  fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
964
965
  unless fixtures.nil?
965
966
  if fixtures.instance_of?(Fixtures)
966
- @loaded_fixtures[fixtures.table_name] = fixtures
967
+ @loaded_fixtures[fixtures.name] = fixtures
967
968
  else
968
- fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
969
+ fixtures.each { |f| @loaded_fixtures[f.name] = f }
969
970
  end
970
971
  end
971
972
  end
@@ -0,0 +1,26 @@
1
+ # Deprecates the use of the former message interpolation syntax in activerecord
2
+ # as in "must have %d characters". The new syntax uses explicit variable names
3
+ # as in "{{value}} must have {{count}} characters".
4
+
5
+ require 'i18n/backend/simple'
6
+ module I18n
7
+ module Backend
8
+ class Simple
9
+ DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
10
+
11
+ protected
12
+ def interpolate_with_deprecated_syntax(locale, string, values = {})
13
+ return string unless string.is_a?(String)
14
+
15
+ string = string.gsub(/%d|%s/) do |s|
16
+ instead = DEPRECATED_INTERPOLATORS[s]
17
+ ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
18
+ instead
19
+ end
20
+
21
+ interpolate_without_deprecated_syntax(locale, string, values)
22
+ end
23
+ alias_method_chain :interpolate, :deprecated_syntax
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ # The values :model, :attribute and :value are always available for interpolation
5
+ # The value :count is available when applicable. Can be used for pluralization.
6
+ messages:
7
+ inclusion: "is not included in the list"
8
+ exclusion: "is reserved"
9
+ invalid: "is invalid"
10
+ confirmation: "doesn't match confirmation"
11
+ accepted: "must be accepted"
12
+ empty: "can't be empty"
13
+ blank: "can't be blank"
14
+ too_long: "is too long (maximum is {{count}} characters)"
15
+ too_short: "is too short (minimum is {{count}} characters)"
16
+ wrong_length: "is the wrong length (should be {{count}} characters)"
17
+ taken: "has already been taken"
18
+ not_a_number: "is not a number"
19
+ greater_than: "must be greater than {{count}}"
20
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
21
+ equal_to: "must be equal to {{count}}"
22
+ less_than: "must be less than {{count}}"
23
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
24
+ odd: "must be odd"
25
+ even: "must be even"
26
+ # Append your own errors here or at the model/attributes scope.
27
+
28
+ # You can define own errors for models or model attributes.
29
+ # The values :model, :attribute and :value are always available for interpolation.
30
+ #
31
+ # For example,
32
+ # models:
33
+ # user:
34
+ # blank: "This is a custom blank message for {{model}}: {{attribute}}"
35
+ # attributes:
36
+ # login:
37
+ # blank: "This is a custom blank message for User login"
38
+ # Will define custom blank validation message for User model and
39
+ # custom blank validation message for login attribute of User model.
40
+ models:
41
+
42
+ # Translate model names. Used in Model.human_name().
43
+ #models:
44
+ # For example,
45
+ # user: "Dude"
46
+ # will translate User model name to "Dude"
47
+
48
+ # Translate model attribute names. Used in Model.human_attribute_name(attribute).
49
+ #attributes:
50
+ # For example,
51
+ # user:
52
+ # login: "Handle"
53
+ # will translate User attribute "login" as "Handle"
54
+
@@ -349,6 +349,27 @@ module ActiveRecord
349
349
  end
350
350
  end
351
351
 
352
+ # MigrationProxy is used to defer loading of the actual migration classes
353
+ # until they are needed
354
+ class MigrationProxy
355
+
356
+ attr_accessor :name, :version, :filename
357
+
358
+ delegate :migrate, :announce, :write, :to=>:migration
359
+
360
+ private
361
+
362
+ def migration
363
+ @migration ||= load_migration
364
+ end
365
+
366
+ def load_migration
367
+ load(filename)
368
+ name.constantize
369
+ end
370
+
371
+ end
372
+
352
373
  class Migrator#:nodoc:
353
374
  class << self
354
375
  def migrate(migrations_path, target_version = nil)
@@ -443,17 +464,25 @@ module ActiveRecord
443
464
  runnable.pop if down? && !target.nil?
444
465
 
445
466
  runnable.each do |migration|
446
- Base.logger.info "Migrating to #{migration} (#{migration.version})"
467
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
447
468
 
448
469
  # On our way up, we skip migrating the ones we've already migrated
449
- # On our way down, we skip reverting the ones we've never migrated
450
470
  next if up? && migrated.include?(migration.version.to_i)
451
471
 
472
+ # On our way down, we skip reverting the ones we've never migrated
452
473
  if down? && !migrated.include?(migration.version.to_i)
453
474
  migration.announce 'never migrated, skipping'; migration.write
454
- else
455
- migration.migrate(@direction)
456
- record_version_state_after_migrating(migration.version)
475
+ next
476
+ end
477
+
478
+ begin
479
+ ddl_transaction do
480
+ migration.migrate(@direction)
481
+ record_version_state_after_migrating(migration.version)
482
+ end
483
+ rescue => e
484
+ canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
485
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
457
486
  end
458
487
  end
459
488
  end
@@ -476,11 +505,10 @@ module ActiveRecord
476
505
  raise DuplicateMigrationNameError.new(name.camelize)
477
506
  end
478
507
 
479
- load(file)
480
-
481
- klasses << returning(name.camelize.constantize) do |klass|
482
- class << klass; attr_accessor :version end
483
- klass.version = version
508
+ klasses << returning(MigrationProxy.new) do |migration|
509
+ migration.name = name.camelize
510
+ migration.version = version
511
+ migration.filename = file
484
512
  end
485
513
  end
486
514
 
@@ -519,5 +547,14 @@ module ActiveRecord
519
547
  def down?
520
548
  @direction == :down
521
549
  end
550
+
551
+ # Wrap the migration in a transaction only if supported by the adapter.
552
+ def ddl_transaction(&block)
553
+ if Base.connection.supports_ddl_transactions?
554
+ Base.transaction { block.call }
555
+ else
556
+ block.call
557
+ end
558
+ end
522
559
  end
523
560
  end
@@ -1,10 +1,10 @@
1
1
  module ActiveRecord
2
2
  module NamedScope
3
- # All subclasses of ActiveRecord::Base have two named_scopes:
4
- # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
5
- # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
3
+ # All subclasses of ActiveRecord::Base have two named \scopes:
4
+ # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
5
+ # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
6
6
  #
7
- # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
7
+ # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
8
8
  # intermediate values (scopes) around as first-class objects is convenient.
9
9
  def self.included(base)
10
10
  base.class_eval do
@@ -26,20 +26,20 @@ module ActiveRecord
26
26
  # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
27
27
  # end
28
28
  #
29
- # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
29
+ # The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
30
30
  # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
31
31
  #
32
- # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
32
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
33
33
  # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
34
34
  # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
35
- # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
36
- # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
35
+ # as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
36
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
37
37
  #
38
- # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
38
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
39
39
  # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
40
40
  # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
41
41
  #
42
- # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
42
+ # All \scopes are available as class methods on the ActiveRecord::Base descendent upon which the \scopes were defined. But they are also available to
43
43
  # <tt>has_many</tt> associations. If,
44
44
  #
45
45
  # class Person < ActiveRecord::Base
@@ -49,7 +49,7 @@ module ActiveRecord
49
49
  # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
50
50
  # only shirts.
51
51
  #
52
- # Named scopes can also be procedural.
52
+ # Named \scopes can also be procedural:
53
53
  #
54
54
  # class Shirt < ActiveRecord::Base
55
55
  # named_scope :colored, lambda { |color|
@@ -59,7 +59,7 @@ module ActiveRecord
59
59
  #
60
60
  # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
61
61
  #
62
- # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
62
+ # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
63
63
  #
64
64
  # class Shirt < ActiveRecord::Base
65
65
  # named_scope :red, :conditions => {:color => 'red'} do
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
  # end
71
71
  #
72
72
  #
73
- # For testing complex named scopes, you can examine the scoping options using the
73
+ # For testing complex named \scopes, you can examine the scoping options using the
74
74
  # <tt>proxy_options</tt> method on the proxy itself.
75
75
  #
76
76
  # class Shirt < ActiveRecord::Base
@@ -101,9 +101,9 @@ module ActiveRecord
101
101
 
102
102
  class Scope
103
103
  attr_reader :proxy_scope, :proxy_options
104
-
104
+ NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
105
105
  [].methods.each do |m|
106
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
106
+ unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
107
107
  delegate m, :to => :proxy_found
108
108
  end
109
109
  end
@@ -136,6 +136,10 @@ module ActiveRecord
136
136
  end
137
137
  end
138
138
 
139
+ def size
140
+ @found ? @found.length : count
141
+ end
142
+
139
143
  def empty?
140
144
  @found ? @found.empty? : count.zero?
141
145
  end
@@ -144,6 +148,14 @@ module ActiveRecord
144
148
  super || @proxy_scope.respond_to?(method, include_private)
145
149
  end
146
150
 
151
+ def any?
152
+ if block_given?
153
+ proxy_found.any? { |*block_args| yield(*block_args) }
154
+ else
155
+ !empty?
156
+ end
157
+ end
158
+
147
159
  protected
148
160
  def proxy_found
149
161
  @found || load_found
@@ -154,7 +166,8 @@ module ActiveRecord
154
166
  if scopes.include?(method)
155
167
  scopes[method].call(self, *args)
156
168
  else
157
- with_scope :find => proxy_options do
169
+ with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
170
+ method = :new if method == :build
158
171
  proxy_scope.send(method, *args, &block)
159
172
  end
160
173
  end