activerecord 3.1.0.beta1 → 3.1.0.rc1

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 (35) hide show
  1. data/CHANGELOG +123 -30
  2. data/README.rdoc +2 -2
  3. data/lib/active_record/associations.rb +10 -9
  4. data/lib/active_record/associations/alias_tracker.rb +2 -2
  5. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  6. data/lib/active_record/associations/builder/singular_association.rb +19 -6
  7. data/lib/active_record/associations/collection_association.rb +61 -52
  8. data/lib/active_record/associations/collection_proxy.rb +30 -3
  9. data/lib/active_record/associations/has_one_association.rb +1 -1
  10. data/lib/active_record/associations/join_dependency/join_association.rb +3 -3
  11. data/lib/active_record/associations/join_helper.rb +1 -1
  12. data/lib/active_record/associations/singular_association.rb +13 -12
  13. data/lib/active_record/associations/through_association.rb +4 -1
  14. data/lib/active_record/base.rb +95 -46
  15. data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -9
  16. data/lib/active_record/connection_adapters/column.rb +1 -1
  17. data/lib/active_record/connection_adapters/mysql2_adapter.rb +8 -3
  18. data/lib/active_record/connection_adapters/postgresql_adapter.rb +28 -26
  19. data/lib/active_record/fixtures.rb +294 -339
  20. data/lib/active_record/identity_map.rb +34 -8
  21. data/lib/active_record/locking/optimistic.rb +16 -16
  22. data/lib/active_record/locking/pessimistic.rb +2 -2
  23. data/lib/active_record/observer.rb +3 -3
  24. data/lib/active_record/persistence.rb +1 -1
  25. data/lib/active_record/railties/controller_runtime.rb +10 -1
  26. data/lib/active_record/railties/databases.rake +9 -9
  27. data/lib/active_record/relation/calculations.rb +5 -6
  28. data/lib/active_record/relation/finder_methods.rb +9 -4
  29. data/lib/active_record/serialization.rb +1 -1
  30. data/lib/active_record/session_store.rb +4 -2
  31. data/lib/active_record/test_case.rb +7 -0
  32. data/lib/active_record/validations.rb +3 -3
  33. data/lib/active_record/version.rb +1 -1
  34. data/lib/rails/generators/active_record/model/templates/migration.rb +1 -1
  35. metadata +6 -6
@@ -64,9 +64,12 @@ module ActiveRecord
64
64
 
65
65
  def method_missing(method, *args, &block)
66
66
  match = DynamicFinderMatch.match(method)
67
- if match && match.creator?
68
- attributes = match.attribute_names
69
- return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
67
+ if match && match.instantiator?
68
+ record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
69
+ @association.send :set_owner_attributes, r
70
+ @association.send :add_to_target, r
71
+ yield(r) if block_given?
72
+ end
70
73
  end
71
74
 
72
75
  if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method))
@@ -120,6 +123,30 @@ module ActiveRecord
120
123
  method_missing(:new, *args, &block)
121
124
  end
122
125
  end
126
+
127
+ def proxy_owner
128
+ ActiveSupport::Deprecation.warn(
129
+ "Calling record.#{@association.reflection.name}.proxy_owner is deprecated. Please use " \
130
+ "record.association(:#{@association.reflection.name}).owner instead."
131
+ )
132
+ @association.owner
133
+ end
134
+
135
+ def proxy_target
136
+ ActiveSupport::Deprecation.warn(
137
+ "Calling record.#{@association.reflection.name}.proxy_target is deprecated. Please use " \
138
+ "record.association(:#{@association.reflection.name}).target instead."
139
+ )
140
+ @association.target
141
+ end
142
+
143
+ def proxy_reflection
144
+ ActiveSupport::Deprecation.warn(
145
+ "Calling record.#{@association.reflection.name}.proxy_reflection is deprecated. Please use " \
146
+ "record.association(:#{@association.reflection.name}).reflection instead."
147
+ )
148
+ @association.reflection
149
+ end
123
150
  end
124
151
  end
125
152
  end
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
 
11
11
  reflection.klass.transaction do
12
12
  if target && target != record
13
- remove_target!(options[:dependent])
13
+ remove_target!(options[:dependent]) unless target.destroyed?
14
14
  end
15
15
 
16
16
  if record
@@ -91,12 +91,12 @@ module ActiveRecord
91
91
 
92
92
  constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
93
93
 
94
- relation.from(join(table, constraint))
95
-
96
94
  unless conditions[i].empty?
97
- relation.where(sanitize(conditions[i], table))
95
+ constraint = constraint.and(sanitize(conditions[i], table))
98
96
  end
99
97
 
98
+ relation.from(join(table, constraint))
99
+
100
100
  # The current table in this iteration becomes the foreign table in the next
101
101
  foreign_table = table
102
102
  end
@@ -32,7 +32,7 @@ module ActiveRecord
32
32
  end
33
33
 
34
34
  def table_alias_for(reflection, join = false)
35
- name = alias_tracker.pluralize(reflection.name)
35
+ name = alias_tracker.pluralize(reflection.name, reflection.active_record)
36
36
  name << "_#{alias_suffix}"
37
37
  name << "_join" if join
38
38
  name
@@ -17,20 +17,28 @@ module ActiveRecord
17
17
  replace(record)
18
18
  end
19
19
 
20
- def create(attributes = {}, options = {})
21
- new_record(:create, attributes, options)
20
+ def create(attributes = {}, options = {}, &block)
21
+ build(attributes, options, &block).tap { |record| record.save }
22
22
  end
23
23
 
24
- def create!(attributes = {}, options = {})
25
- build(attributes, options).tap { |record| record.save! }
24
+ def create!(attributes = {}, options = {}, &block)
25
+ build(attributes, options, &block).tap { |record| record.save! }
26
26
  end
27
27
 
28
28
  def build(attributes = {}, options = {})
29
- new_record(:build, attributes, options)
29
+ record = reflection.build_association(attributes, options)
30
+ record.assign_attributes(create_scope.except(*record.changed), :without_protection => true)
31
+ yield(record) if block_given?
32
+ set_new_record(record)
33
+ record
30
34
  end
31
35
 
32
36
  private
33
37
 
38
+ def create_scope
39
+ scoped.scope_for_create.stringify_keys.except(klass.primary_key)
40
+ end
41
+
34
42
  def find_target
35
43
  scoped.first.tap { |record| set_inverse_instance(record) }
36
44
  end
@@ -43,13 +51,6 @@ module ActiveRecord
43
51
  def set_new_record(record)
44
52
  replace(record)
45
53
  end
46
-
47
- def new_record(method, attributes, options)
48
- attributes = scoped.scope_for_create.merge(attributes || {})
49
- record = reflection.send("#{method}_association", attributes, options)
50
- set_new_record(record)
51
- record
52
- end
53
54
  end
54
55
  end
55
56
  end
@@ -14,7 +14,10 @@ module ActiveRecord
14
14
  def target_scope
15
15
  scope = super
16
16
  chain[1..-1].each do |reflection|
17
- scope = scope.merge(reflection.klass.scoped)
17
+ scope = scope.merge(
18
+ reflection.klass.scoped.with_default_scope.
19
+ except(:select, :create_with)
20
+ )
18
21
  end
19
22
  scope
20
23
  end
@@ -393,8 +393,8 @@ module ActiveRecord #:nodoc:
393
393
  # Indicates whether table names should be the pluralized versions of the corresponding class names.
394
394
  # If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
395
395
  # See table_name for the full rules on table/class naming. This is true, by default.
396
- cattr_accessor :pluralize_table_names, :instance_writer => false
397
- @@pluralize_table_names = true
396
+ class_attribute :pluralize_table_names, :instance_writer => false
397
+ self.pluralize_table_names = true
398
398
 
399
399
  ##
400
400
  # :singleton-method:
@@ -482,7 +482,7 @@ module ActiveRecord #:nodoc:
482
482
  # # Create a single new object
483
483
  # User.create(:first_name => 'Jamie')
484
484
  #
485
- # # Create a single new object using the :admin mass-assignment security scope
485
+ # # Create a single new object using the :admin mass-assignment security role
486
486
  # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
487
487
  #
488
488
  # # Create a single new object bypassing mass-assignment security
@@ -577,15 +577,25 @@ module ActiveRecord #:nodoc:
577
577
  #
578
578
  # ==== Examples
579
579
  #
580
- # class Invoice < ActiveRecord::Base; end;
580
+ # class Invoice < ActiveRecord::Base
581
+ # end
582
+ #
581
583
  # file class table_name
582
584
  # invoice.rb Invoice invoices
583
585
  #
584
- # class Invoice < ActiveRecord::Base; class Lineitem < ActiveRecord::Base; end; end;
586
+ # class Invoice < ActiveRecord::Base
587
+ # class Lineitem < ActiveRecord::Base
588
+ # end
589
+ # end
590
+ #
585
591
  # file class table_name
586
592
  # invoice.rb Invoice::Lineitem invoice_lineitems
587
593
  #
588
- # module Invoice; class Lineitem < ActiveRecord::Base; end; end;
594
+ # module Invoice
595
+ # class Lineitem < ActiveRecord::Base
596
+ # end
597
+ # end
598
+ #
589
599
  # file class table_name
590
600
  # invoice/lineitem.rb Invoice::Lineitem lineitems
591
601
  #
@@ -767,6 +777,17 @@ module ActiveRecord #:nodoc:
767
777
  super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
768
778
  end
769
779
 
780
+ # Returns an array of column names as strings if it's not
781
+ # an abstract class and table exists.
782
+ # Otherwise it returns an empty array.
783
+ def attribute_names
784
+ @attribute_names ||= if !abstract_class? && table_exists?
785
+ column_names
786
+ else
787
+ []
788
+ end
789
+ end
790
+
770
791
  # Set the lookup ancestors for ActiveModel.
771
792
  def lookup_ancestors #:nodoc:
772
793
  klass = self
@@ -830,6 +851,10 @@ module ActiveRecord #:nodoc:
830
851
  @symbolized_base_class ||= base_class.to_s.to_sym
831
852
  end
832
853
 
854
+ def symbolized_sti_name
855
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
856
+ end
857
+
833
858
  # Returns the base AR subclass that this class descends from. If A
834
859
  # extends AR::Base, A.base_class will return A. If B descends from A
835
860
  # through some arbitrarily deep hierarchy, B.base_class will return A.
@@ -891,7 +916,7 @@ module ActiveRecord #:nodoc:
891
916
  # not use the default_scope:
892
917
  #
893
918
  # Post.unscoped {
894
- # limit(10) # Fires "SELECT * FROM posts LIMIT 10"
919
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
895
920
  # }
896
921
  #
897
922
  # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
@@ -1482,7 +1507,7 @@ MSG
1482
1507
  # # Instantiates a single new object
1483
1508
  # User.new(:first_name => 'Jamie')
1484
1509
  #
1485
- # # Instantiates a single new object using the :admin mass-assignment security scope
1510
+ # # Instantiates a single new object using the :admin mass-assignment security role
1486
1511
  # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
1487
1512
  #
1488
1513
  # # Instantiates a single new object bypassing mass-assignment security
@@ -1648,17 +1673,16 @@ MSG
1648
1673
 
1649
1674
  return unless new_attributes.is_a?(Hash)
1650
1675
 
1651
- guard_protected_attributes ||= true
1652
- if guard_protected_attributes
1653
- assign_attributes(new_attributes)
1654
- else
1676
+ if guard_protected_attributes == false
1655
1677
  assign_attributes(new_attributes, :without_protection => true)
1678
+ else
1679
+ assign_attributes(new_attributes)
1656
1680
  end
1657
1681
  end
1658
1682
 
1659
1683
  # Allows you to set all the attributes for a particular mass-assignment
1660
- # security scope by passing in a hash of attributes with keys matching
1661
- # the attribute names (which again matches the column names) and the scope
1684
+ # security role by passing in a hash of attributes with keys matching
1685
+ # the attribute names (which again matches the column names) and the role
1662
1686
  # name using the :as option.
1663
1687
  #
1664
1688
  # To bypass mass-assignment security you can use the :without_protection => true
@@ -1684,13 +1708,15 @@ MSG
1684
1708
  # user.name # => "Josh"
1685
1709
  # user.is_admin? # => true
1686
1710
  def assign_attributes(new_attributes, options = {})
1711
+ return unless new_attributes
1712
+
1687
1713
  attributes = new_attributes.stringify_keys
1688
- scope = options[:as] || :default
1714
+ role = options[:as] || :default
1689
1715
 
1690
1716
  multi_parameter_attributes = []
1691
1717
 
1692
1718
  unless options[:without_protection]
1693
- attributes = sanitize_for_mass_assignment(attributes, scope)
1719
+ attributes = sanitize_for_mass_assignment(attributes, role)
1694
1720
  end
1695
1721
 
1696
1722
  attributes.each do |k, v|
@@ -1943,32 +1969,9 @@ MSG
1943
1969
  errors = []
1944
1970
  callstack.each do |name, values_with_empty_parameters|
1945
1971
  begin
1946
- klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
1947
- # in order to allow a date to be set without a year, we must keep the empty values.
1948
- # Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
1949
- values = values_with_empty_parameters.reject { |v| v.nil? }
1950
-
1951
- if values.empty?
1952
- send(name + "=", nil)
1953
- else
1954
-
1955
- value = if Time == klass
1956
- instantiate_time_object(name, values)
1957
- elsif Date == klass
1958
- begin
1959
- values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
1960
- Date.new(*values)
1961
- rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
1962
- instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
1963
- end
1964
- else
1965
- klass.new(*values)
1966
- end
1967
-
1968
- send(name + "=", value)
1969
- end
1972
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
1970
1973
  rescue => ex
1971
- errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
1974
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
1972
1975
  end
1973
1976
  end
1974
1977
  unless errors.empty?
@@ -1976,19 +1979,65 @@ MSG
1976
1979
  end
1977
1980
  end
1978
1981
 
1982
+ def read_value_from_parameter(name, values_hash_from_param)
1983
+ klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
1984
+ if values_hash_from_param.values.all?{|v|v.nil?}
1985
+ nil
1986
+ elsif klass == Time
1987
+ read_time_parameter_value(name, values_hash_from_param)
1988
+ elsif klass == Date
1989
+ read_date_parameter_value(name, values_hash_from_param)
1990
+ else
1991
+ read_other_parameter_value(klass, name, values_hash_from_param)
1992
+ end
1993
+ end
1994
+
1995
+ def read_time_parameter_value(name, values_hash_from_param)
1996
+ # If Date bits were not provided, error
1997
+ raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
1998
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
1999
+ set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
2000
+ # If Date bits were provided but blank, then default to 1
2001
+ # If Time bits are not there, then default to 0
2002
+ [1,1,1,0,0,0].each_with_index{|v,i| set_values[i] = set_values[i].blank? ? v : set_values[i]}
2003
+ instantiate_time_object(name, set_values)
2004
+ end
2005
+
2006
+ def read_date_parameter_value(name, values_hash_from_param)
2007
+ set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]}
2008
+ begin
2009
+ Date.new(*set_values)
2010
+ rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
2011
+ instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
2012
+ end
2013
+ end
2014
+
2015
+ def read_other_parameter_value(klass, name, values_hash_from_param)
2016
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
2017
+ values = (1..max_position).collect do |position|
2018
+ raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
2019
+ values_hash_from_param[position]
2020
+ end
2021
+ klass.new(*values)
2022
+ end
2023
+
2024
+ def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
2025
+ [values_hash_from_param.keys.max,upper_cap].min
2026
+ end
2027
+
1979
2028
  def extract_callstack_for_multiparameter_attributes(pairs)
1980
2029
  attributes = { }
1981
2030
 
1982
- for pair in pairs
2031
+ pairs.each do |pair|
1983
2032
  multiparameter_name, value = pair
1984
2033
  attribute_name = multiparameter_name.split("(").first
1985
- attributes[attribute_name] = [] unless attributes.include?(attribute_name)
2034
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
1986
2035
 
1987
2036
  parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
1988
- attributes[attribute_name] << [ find_parameter_position(multiparameter_name), parameter_value ]
2037
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
1989
2038
  end
1990
2039
 
1991
- attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
2040
+ attributes
1992
2041
  end
1993
2042
 
1994
2043
  def type_cast_attribute_value(multiparameter_name, value)
@@ -1996,7 +2045,7 @@ MSG
1996
2045
  end
1997
2046
 
1998
2047
  def find_parameter_position(multiparameter_name)
1999
- multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
2048
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
2000
2049
  end
2001
2050
 
2002
2051
  # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
@@ -6,15 +6,7 @@ module ActiveRecord
6
6
  # Returns an array of record hashes with the column names as keys and
7
7
  # column values as values.
8
8
  def select_all(sql, name = nil, binds = [])
9
- if supports_statement_cache?
10
- select(sql, name, binds)
11
- else
12
- return select(sql, name) if binds.empty?
13
- binds = binds.dup
14
- select sql.gsub('?') {
15
- quote(*binds.shift.reverse)
16
- }, name
17
- end
9
+ select(sql, name, binds)
18
10
  end
19
11
 
20
12
  # Returns a record hash with the column names as keys and column values
@@ -189,7 +189,7 @@ module ActiveRecord
189
189
 
190
190
  def new_time(year, mon, mday, hour, min, sec, microsec)
191
191
  # Treat 0000-00-00 00:00:00 as nil.
192
- return nil if year.nil? || year == 0
192
+ return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
193
193
 
194
194
  Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
195
195
  end
@@ -184,6 +184,10 @@ module ActiveRecord
184
184
  QUOTED_FALSE
185
185
  end
186
186
 
187
+ def substitute_at(column, index)
188
+ Arel.sql "\0"
189
+ end
190
+
187
191
  # REFERENTIAL INTEGRITY ====================================
188
192
 
189
193
  def disable_referential_integrity(&block) #:nodoc:
@@ -292,14 +296,14 @@ module ActiveRecord
292
296
  binds = binds.dup
293
297
 
294
298
  # Pretend to support bind parameters
295
- execute sql.gsub('?') { quote(*binds.shift.reverse) }, name
299
+ execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name
296
300
  end
297
301
 
298
302
  def exec_delete(sql, name, binds)
299
303
  binds = binds.dup
300
304
 
301
305
  # Pretend to support bind parameters
302
- execute sql.gsub('?') { quote(*binds.shift.reverse) }, name
306
+ execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name
303
307
  @connection.affected_rows
304
308
  end
305
309
  alias :exec_update :exec_delete
@@ -646,7 +650,8 @@ module ActiveRecord
646
650
  # Returns an array of record hashes with the column names as keys and
647
651
  # column values as values.
648
652
  def select(sql, name = nil, binds = [])
649
- exec_query(sql, name, binds).to_a
653
+ binds = binds.dup
654
+ exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
650
655
  end
651
656
 
652
657
  def exec_query(sql, name = 'SQL', binds = [])