activerecord 3.1.12 → 3.2.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 (99) hide show
  1. data/CHANGELOG.md +6263 -103
  2. data/README.rdoc +2 -2
  3. data/examples/performance.rb +55 -31
  4. data/lib/active_record.rb +28 -2
  5. data/lib/active_record/aggregations.rb +2 -2
  6. data/lib/active_record/associations.rb +82 -69
  7. data/lib/active_record/associations/association.rb +2 -37
  8. data/lib/active_record/associations/association_scope.rb +3 -30
  9. data/lib/active_record/associations/builder/association.rb +6 -4
  10. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  11. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +4 -4
  13. data/lib/active_record/associations/builder/has_one.rb +5 -6
  14. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  15. data/lib/active_record/associations/collection_association.rb +55 -28
  16. data/lib/active_record/associations/collection_proxy.rb +1 -35
  17. data/lib/active_record/associations/has_many_association.rb +5 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +11 -8
  19. data/lib/active_record/associations/join_dependency.rb +1 -1
  20. data/lib/active_record/associations/preloader/association.rb +3 -1
  21. data/lib/active_record/attribute_assignment.rb +221 -0
  22. data/lib/active_record/attribute_methods.rb +212 -32
  23. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  24. data/lib/active_record/attribute_methods/dirty.rb +3 -3
  25. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  26. data/lib/active_record/attribute_methods/read.rb +69 -80
  27. data/lib/active_record/attribute_methods/serialization.rb +89 -0
  28. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -14
  29. data/lib/active_record/attribute_methods/write.rb +27 -5
  30. data/lib/active_record/autosave_association.rb +23 -8
  31. data/lib/active_record/base.rb +223 -1712
  32. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +98 -132
  33. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +82 -29
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +13 -42
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +36 -25
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +41 -13
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +78 -43
  40. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  41. data/lib/active_record/connection_adapters/mysql2_adapter.rb +138 -578
  42. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -658
  43. data/lib/active_record/connection_adapters/postgresql_adapter.rb +144 -94
  44. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  45. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  46. data/lib/active_record/connection_adapters/sqlite_adapter.rb +43 -22
  47. data/lib/active_record/counter_cache.rb +1 -1
  48. data/lib/active_record/dynamic_matchers.rb +79 -0
  49. data/lib/active_record/errors.rb +11 -1
  50. data/lib/active_record/explain.rb +83 -0
  51. data/lib/active_record/explain_subscriber.rb +21 -0
  52. data/lib/active_record/fixtures.rb +31 -76
  53. data/lib/active_record/fixtures/file.rb +65 -0
  54. data/lib/active_record/identity_map.rb +1 -7
  55. data/lib/active_record/inheritance.rb +167 -0
  56. data/lib/active_record/integration.rb +49 -0
  57. data/lib/active_record/locking/optimistic.rb +19 -11
  58. data/lib/active_record/locking/pessimistic.rb +1 -1
  59. data/lib/active_record/log_subscriber.rb +3 -3
  60. data/lib/active_record/migration.rb +38 -29
  61. data/lib/active_record/migration/command_recorder.rb +7 -7
  62. data/lib/active_record/model_schema.rb +362 -0
  63. data/lib/active_record/nested_attributes.rb +3 -2
  64. data/lib/active_record/persistence.rb +51 -1
  65. data/lib/active_record/querying.rb +58 -0
  66. data/lib/active_record/railtie.rb +24 -28
  67. data/lib/active_record/railties/controller_runtime.rb +3 -1
  68. data/lib/active_record/railties/databases.rake +133 -77
  69. data/lib/active_record/readonly_attributes.rb +26 -0
  70. data/lib/active_record/reflection.rb +7 -15
  71. data/lib/active_record/relation.rb +78 -35
  72. data/lib/active_record/relation/batches.rb +5 -2
  73. data/lib/active_record/relation/calculations.rb +27 -6
  74. data/lib/active_record/relation/delegation.rb +49 -0
  75. data/lib/active_record/relation/finder_methods.rb +5 -4
  76. data/lib/active_record/relation/predicate_builder.rb +13 -16
  77. data/lib/active_record/relation/query_methods.rb +59 -4
  78. data/lib/active_record/result.rb +1 -1
  79. data/lib/active_record/sanitization.rb +194 -0
  80. data/lib/active_record/schema_dumper.rb +5 -2
  81. data/lib/active_record/scoping.rb +152 -0
  82. data/lib/active_record/scoping/default.rb +140 -0
  83. data/lib/active_record/scoping/named.rb +202 -0
  84. data/lib/active_record/serialization.rb +1 -43
  85. data/lib/active_record/serializers/xml_serializer.rb +2 -44
  86. data/lib/active_record/session_store.rb +11 -11
  87. data/lib/active_record/store.rb +50 -0
  88. data/lib/active_record/test_case.rb +11 -7
  89. data/lib/active_record/timestamp.rb +16 -3
  90. data/lib/active_record/transactions.rb +5 -5
  91. data/lib/active_record/translation.rb +22 -0
  92. data/lib/active_record/validations.rb +1 -1
  93. data/lib/active_record/validations/associated.rb +5 -4
  94. data/lib/active_record/validations/uniqueness.rb +4 -4
  95. data/lib/active_record/version.rb +3 -3
  96. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  97. metadata +48 -38
  98. checksums.yaml +0 -7
  99. data/lib/active_record/named_scope.rb +0 -200
@@ -23,7 +23,7 @@ module ActiveRecord
23
23
  #
24
24
  # If the association has a counter cache it gets that value. Otherwise
25
25
  # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
26
- # there's one. Some configuration options like :group make it impossible
26
+ # there's one. Some configuration options like :group make it impossible
27
27
  # to do an SQL count, in those cases the array count will be used.
28
28
  #
29
29
  # That does not depend on whether the collection has already been loaded
@@ -99,6 +99,10 @@ module ActiveRecord
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ def foreign_key_present?
104
+ owner.attribute_present?(reflection.association_primary_key)
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -8,7 +8,9 @@ module ActiveRecord
8
8
 
9
9
  def initialize(owner, reflection)
10
10
  super
11
- @through_records = {}
11
+
12
+ @through_records = {}
13
+ @through_association = nil
12
14
  end
13
15
 
14
16
  # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
@@ -59,7 +61,7 @@ module ActiveRecord
59
61
  private
60
62
 
61
63
  def through_association
62
- owner.association(through_reflection.name)
64
+ @through_association ||= owner.association(through_reflection.name)
63
65
  end
64
66
 
65
67
  # We temporarily cache through record that has been build, because if we build a
@@ -122,8 +124,7 @@ module ActiveRecord
122
124
  def delete_records(records, method)
123
125
  ensure_not_nested
124
126
 
125
- through = through_association
126
- scope = through.scoped.where(construct_join_attributes(*records))
127
+ scope = through_association.scoped.where(construct_join_attributes(*records))
127
128
 
128
129
  case method
129
130
  when :destroy
@@ -134,7 +135,7 @@ module ActiveRecord
134
135
  count = scope.delete_all
135
136
  end
136
137
 
137
- delete_through_records(through, records)
138
+ delete_through_records(records)
138
139
 
139
140
  if through_reflection.macro == :has_many && update_through_counter?(method)
140
141
  update_counter(-count, through_reflection)
@@ -149,14 +150,16 @@ module ActiveRecord
149
150
  candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
150
151
  end
151
152
 
152
- def delete_through_records(through, records)
153
+ def delete_through_records(records)
153
154
  records.each do |record|
154
155
  through_records = through_records_for(record)
155
156
 
156
157
  if through_reflection.macro == :has_many
157
- through_records.each { |r| through.target.delete(r) }
158
+ through_records.each { |r| through_association.target.delete(r) }
158
159
  else
159
- through.target = nil if through_records.include?(through.target)
160
+ if through_records.include?(through_association.target)
161
+ through_association.target = nil
162
+ end
160
163
  end
161
164
 
162
165
  @through_records.delete(record.object_id)
@@ -184,7 +184,7 @@ module ActiveRecord
184
184
 
185
185
  macro = join_part.reflection.macro
186
186
  if macro == :has_one
187
- return if record.association_cache.key?(join_part.reflection.name)
187
+ return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
188
188
  association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
189
189
  set_target_and_inverse(join_part, association, record)
190
190
  else
@@ -117,7 +117,9 @@ module ActiveRecord
117
117
  conditions = klass.send(:instance_eval, &conditions)
118
118
  end
119
119
 
120
- conditions
120
+ if conditions
121
+ klass.send(:sanitize_sql, conditions)
122
+ end
121
123
  end
122
124
  end
123
125
  end
@@ -0,0 +1,221 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord
4
+ module AttributeAssignment
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::MassAssignmentSecurity
7
+
8
+ module ClassMethods
9
+ private
10
+
11
+ # The primary key and inheritance column can never be set by mass-assignment for security reasons.
12
+ def attributes_protected_by_default
13
+ default = [ primary_key, inheritance_column ]
14
+ default << 'id' unless primary_key.eql? 'id'
15
+ default
16
+ end
17
+ end
18
+
19
+ # Allows you to set all the attributes at once by passing in a hash with keys
20
+ # matching the attribute names (which again matches the column names).
21
+ #
22
+ # If any attributes are protected by either +attr_protected+ or
23
+ # +attr_accessible+ then only settable attributes will be assigned.
24
+ #
25
+ # class User < ActiveRecord::Base
26
+ # attr_protected :is_admin
27
+ # end
28
+ #
29
+ # user = User.new
30
+ # user.attributes = { :username => 'Phusion', :is_admin => true }
31
+ # user.username # => "Phusion"
32
+ # user.is_admin? # => false
33
+ def attributes=(new_attributes)
34
+ return unless new_attributes.is_a?(Hash)
35
+
36
+ assign_attributes(new_attributes)
37
+ end
38
+
39
+ # Allows you to set all the attributes for a particular mass-assignment
40
+ # security role by passing in a hash of attributes with keys matching
41
+ # the attribute names (which again matches the column names) and the role
42
+ # name using the :as option.
43
+ #
44
+ # To bypass mass-assignment security you can use the :without_protection => true
45
+ # option.
46
+ #
47
+ # class User < ActiveRecord::Base
48
+ # attr_accessible :name
49
+ # attr_accessible :name, :is_admin, :as => :admin
50
+ # end
51
+ #
52
+ # user = User.new
53
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true })
54
+ # user.name # => "Josh"
55
+ # user.is_admin? # => false
56
+ #
57
+ # user = User.new
58
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
59
+ # user.name # => "Josh"
60
+ # user.is_admin? # => true
61
+ #
62
+ # user = User.new
63
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
64
+ # user.name # => "Josh"
65
+ # user.is_admin? # => true
66
+ def assign_attributes(new_attributes, options = {})
67
+ return unless new_attributes
68
+
69
+ attributes = new_attributes.stringify_keys
70
+ multi_parameter_attributes = []
71
+ nested_parameter_attributes = []
72
+ @mass_assignment_options = options
73
+
74
+ unless options[:without_protection]
75
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
76
+ end
77
+
78
+ attributes.each do |k, v|
79
+ if k.include?("(")
80
+ multi_parameter_attributes << [ k, v ]
81
+ elsif respond_to?("#{k}=")
82
+ if v.is_a?(Hash)
83
+ nested_parameter_attributes << [ k, v ]
84
+ else
85
+ send("#{k}=", v)
86
+ end
87
+ else
88
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
89
+ end
90
+ end
91
+
92
+ # assign any deferred nested attributes after the base attributes have been set
93
+ nested_parameter_attributes.each do |k,v|
94
+ send("#{k}=", v)
95
+ end
96
+
97
+ @mass_assignment_options = nil
98
+ assign_multiparameter_attributes(multi_parameter_attributes)
99
+ end
100
+
101
+ protected
102
+
103
+ def mass_assignment_options
104
+ @mass_assignment_options ||= {}
105
+ end
106
+
107
+ def mass_assignment_role
108
+ mass_assignment_options[:as] || :default
109
+ end
110
+
111
+ private
112
+
113
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
114
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
115
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
116
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
117
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
118
+ # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
119
+ # attribute will be set to nil.
120
+ def assign_multiparameter_attributes(pairs)
121
+ execute_callstack_for_multiparameter_attributes(
122
+ extract_callstack_for_multiparameter_attributes(pairs)
123
+ )
124
+ end
125
+
126
+ def instantiate_time_object(name, values)
127
+ if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
128
+ Time.zone.local(*values)
129
+ else
130
+ Time.time_with_datetime_fallback(self.class.default_timezone, *values)
131
+ end
132
+ end
133
+
134
+ def execute_callstack_for_multiparameter_attributes(callstack)
135
+ errors = []
136
+ callstack.each do |name, values_with_empty_parameters|
137
+ begin
138
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
139
+ rescue => ex
140
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
141
+ end
142
+ end
143
+ unless errors.empty?
144
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
145
+ end
146
+ end
147
+
148
+ def read_value_from_parameter(name, values_hash_from_param)
149
+ klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
150
+ if values_hash_from_param.values.all?{|v|v.nil?}
151
+ nil
152
+ elsif klass == Time
153
+ read_time_parameter_value(name, values_hash_from_param)
154
+ elsif klass == Date
155
+ read_date_parameter_value(name, values_hash_from_param)
156
+ else
157
+ read_other_parameter_value(klass, name, values_hash_from_param)
158
+ end
159
+ end
160
+
161
+ def read_time_parameter_value(name, values_hash_from_param)
162
+ # If Date bits were not provided, error
163
+ raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
164
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
165
+ # If Date bits were provided but blank, then return nil
166
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
167
+
168
+ set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
169
+ # If Time bits are not there, then default to 0
170
+ (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
171
+ instantiate_time_object(name, set_values)
172
+ end
173
+
174
+ def read_date_parameter_value(name, values_hash_from_param)
175
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
176
+ set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
177
+ begin
178
+ Date.new(*set_values)
179
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
180
+ 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
181
+ end
182
+ end
183
+
184
+ def read_other_parameter_value(klass, name, values_hash_from_param)
185
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
186
+ values = (1..max_position).collect do |position|
187
+ raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
188
+ values_hash_from_param[position]
189
+ end
190
+ klass.new(*values)
191
+ end
192
+
193
+ def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
194
+ [values_hash_from_param.keys.max,upper_cap].min
195
+ end
196
+
197
+ def extract_callstack_for_multiparameter_attributes(pairs)
198
+ attributes = { }
199
+
200
+ pairs.each do |pair|
201
+ multiparameter_name, value = pair
202
+ attribute_name = multiparameter_name.split("(").first
203
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
204
+
205
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
206
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
207
+ end
208
+
209
+ attributes
210
+ end
211
+
212
+ def type_cast_attribute_value(multiparameter_name, value)
213
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
214
+ end
215
+
216
+ def find_parameter_position(multiparameter_name)
217
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
218
+ end
219
+
220
+ end
221
+ end
@@ -1,4 +1,5 @@
1
1
  require 'active_support/core_ext/enumerable'
2
+ require 'active_support/deprecation'
2
3
 
3
4
  module ActiveRecord
4
5
  # = Active Record Attribute Methods
@@ -6,69 +7,248 @@ module ActiveRecord
6
7
  extend ActiveSupport::Concern
7
8
  include ActiveModel::AttributeMethods
8
9
 
10
+ included do
11
+ include Read
12
+ include Write
13
+ include BeforeTypeCast
14
+ include Query
15
+ include PrimaryKey
16
+ include TimeZoneConversion
17
+ include Dirty
18
+ include Serialization
19
+ include DeprecatedUnderscoreRead
20
+
21
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
22
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
23
+ # (Alias for the protected read_attribute method).
24
+ alias [] read_attribute
25
+
26
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
27
+ # (Alias for the protected write_attribute method).
28
+ alias []= write_attribute
29
+
30
+ public :[], :[]=
31
+ end
32
+
9
33
  module ClassMethods
10
34
  # Generates all the attribute related methods for columns in the database
11
35
  # accessors, mutators and query methods.
12
36
  def define_attribute_methods
13
37
  return if attribute_methods_generated?
14
- super(column_names)
15
- @attribute_methods_generated = true
38
+
39
+ if base_class == self
40
+ super(column_names)
41
+ @attribute_methods_generated = true
42
+ else
43
+ base_class.define_attribute_methods
44
+ end
16
45
  end
17
46
 
18
47
  def attribute_methods_generated?
19
- @attribute_methods_generated ||= false
48
+ if base_class == self
49
+ @attribute_methods_generated ||= false
50
+ else
51
+ base_class.attribute_methods_generated?
52
+ end
20
53
  end
21
54
 
22
- def undefine_attribute_methods(*args)
23
- super
24
- @attribute_methods_generated = false
55
+ def generated_attribute_methods
56
+ @generated_attribute_methods ||= (base_class == self ? super : base_class.generated_attribute_methods)
57
+ end
58
+
59
+ def generated_external_attribute_methods
60
+ @generated_external_attribute_methods ||= begin
61
+ if base_class == self
62
+ # We will define the methods as instance methods, but will call them as singleton
63
+ # methods. This allows us to use method_defined? to check if the method exists,
64
+ # which is fast and won't give any false positives from the ancestors (because
65
+ # there are no ancestors).
66
+ Module.new { extend self }
67
+ else
68
+ base_class.generated_external_attribute_methods
69
+ end
70
+ end
71
+ end
72
+
73
+ def undefine_attribute_methods
74
+ if base_class == self
75
+ super
76
+ @attribute_methods_generated = false
77
+ else
78
+ base_class.undefine_attribute_methods
79
+ end
25
80
  end
26
81
 
27
- # Checks whether the method is defined in the model or any of its subclasses
28
- # that also derive from Active Record. Raises DangerousAttributeError if the
29
- # method is defined by Active Record though.
30
82
  def instance_method_already_implemented?(method_name)
31
- method_name = method_name.to_s
32
- index = ancestors.index(ActiveRecord::Base) || ancestors.length
33
- @_defined_class_methods ||= ancestors.first(index).map { |m|
34
- m.instance_methods(false) | m.private_instance_methods(false)
35
- }.flatten.map {|m| m.to_s }.to_set
36
-
37
- @@_defined_activerecord_methods ||= defined_activerecord_methods
38
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
39
- @_defined_class_methods.include?(method_name)
83
+ if dangerous_attribute_method?(method_name)
84
+ raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
85
+ end
86
+
87
+ super
40
88
  end
41
89
 
42
- def defined_activerecord_methods
90
+ # A method name is 'dangerous' if it is already defined by Active Record, but
91
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
92
+ def dangerous_attribute_method?(method_name)
43
93
  active_record = ActiveRecord::Base
44
- super_klass = ActiveRecord::Base.superclass
45
- methods = (active_record.instance_methods - super_klass.instance_methods) +
46
- (active_record.private_instance_methods - super_klass.private_instance_methods)
47
- methods.map {|m| m.to_s }.to_set
94
+ superclass = ActiveRecord::Base.superclass
95
+
96
+ (active_record.method_defined?(method_name) ||
97
+ active_record.private_method_defined?(method_name)) &&
98
+ !superclass.method_defined?(method_name) &&
99
+ !superclass.private_method_defined?(method_name)
100
+ end
101
+
102
+ def attribute_method?(attribute)
103
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
104
+ end
105
+
106
+ # Returns an array of column names as strings if it's not
107
+ # an abstract class and table exists.
108
+ # Otherwise it returns an empty array.
109
+ def attribute_names
110
+ @attribute_names ||= if !abstract_class? && table_exists?
111
+ column_names
112
+ else
113
+ []
114
+ end
48
115
  end
49
116
  end
50
117
 
51
- def method_missing(method_id, *args, &block)
52
- # If we haven't generated any methods yet, generate them, then
53
- # see if we've created the method we're looking for.
54
- if !self.class.attribute_methods_generated?
118
+ # If we haven't generated any methods yet, generate them, then
119
+ # see if we've created the method we're looking for.
120
+ def method_missing(method, *args, &block)
121
+ unless self.class.attribute_methods_generated?
55
122
  self.class.define_attribute_methods
56
- method_name = method_id.to_s
57
- guard_private_attribute_method!(method_name, args)
58
- send(method_id, *args, &block)
123
+
124
+ if respond_to_without_attributes?(method)
125
+ send(method, *args, &block)
126
+ else
127
+ super
128
+ end
59
129
  else
60
130
  super
61
131
  end
62
132
  end
63
133
 
134
+ def attribute_missing(match, *args, &block)
135
+ if self.class.columns_hash[match.attr_name]
136
+ ActiveSupport::Deprecation.warn(
137
+ "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
138
+ "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
139
+ "is a column of the table. If this error has happened through normal usage of Active " \
140
+ "Record (rather than through your own code or external libraries), please report it as " \
141
+ "a bug."
142
+ )
143
+ end
144
+
145
+ super
146
+ end
147
+
64
148
  def respond_to?(name, include_private = false)
65
149
  self.class.define_attribute_methods unless self.class.attribute_methods_generated?
66
150
  super
67
151
  end
68
152
 
153
+ # Returns true if the given attribute is in the attributes hash
154
+ def has_attribute?(attr_name)
155
+ @attributes.has_key?(attr_name.to_s)
156
+ end
157
+
158
+ # Returns an array of names for the attributes available on this object.
159
+ def attribute_names
160
+ @attributes.keys
161
+ end
162
+
163
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
164
+ def attributes
165
+ Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
166
+ end
167
+
168
+ # Returns an <tt>#inspect</tt>-like string for the value of the
169
+ # attribute +attr_name+. String attributes are truncated upto 50
170
+ # characters, and Date and Time attributes are returned in the
171
+ # <tt>:db</tt> format. Other attributes return the value of
172
+ # <tt>#inspect</tt> without modification.
173
+ #
174
+ # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
175
+ #
176
+ # person.attribute_for_inspect(:name)
177
+ # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
178
+ #
179
+ # person.attribute_for_inspect(:created_at)
180
+ # # => '"2009-01-12 04:48:57"'
181
+ def attribute_for_inspect(attr_name)
182
+ value = read_attribute(attr_name)
183
+
184
+ if value.is_a?(String) && value.length > 50
185
+ "#{value[0..50]}...".inspect
186
+ elsif value.is_a?(Date) || value.is_a?(Time)
187
+ %("#{value.to_s(:db)}")
188
+ else
189
+ value.inspect
190
+ end
191
+ end
192
+
193
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
194
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
195
+ def attribute_present?(attribute)
196
+ value = read_attribute(attribute)
197
+ !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
198
+ end
199
+
200
+ # Returns the column object for the named attribute.
201
+ def column_for_attribute(name)
202
+ self.class.columns_hash[name.to_s]
203
+ end
204
+
69
205
  protected
70
- def attribute_method?(attr_name)
71
- attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
206
+
207
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
208
+ attribute_names.each do |name|
209
+ attributes[name] = clone_attribute_value(reader_method, name)
72
210
  end
211
+ attributes
212
+ end
213
+
214
+ def clone_attribute_value(reader_method, attribute_name)
215
+ value = send(reader_method, attribute_name)
216
+ value.duplicable? ? value.clone : value
217
+ rescue TypeError, NoMethodError
218
+ value
219
+ end
220
+
221
+ # Returns a copy of the attributes hash where all the values have been safely quoted for use in
222
+ # an Arel insert/update method.
223
+ def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
224
+ attrs = {}
225
+ klass = self.class
226
+ arel_table = klass.arel_table
227
+
228
+ attribute_names.each do |name|
229
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
230
+
231
+ if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
232
+
233
+ value = if klass.serialized_attributes.include?(name)
234
+ @attributes[name].serialized_value
235
+ else
236
+ # FIXME: we need @attributes to be used consistently.
237
+ # If the values stored in @attributes were already type
238
+ # casted, this code could be simplified
239
+ read_attribute(name)
240
+ end
241
+
242
+ attrs[arel_table[name]] = value
243
+ end
244
+ end
245
+ end
246
+
247
+ attrs
248
+ end
249
+
250
+ def attribute_method?(attr_name)
251
+ attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
252
+ end
73
253
  end
74
254
  end