activerecord 1.15.6 → 2.0.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 (185) hide show
  1. data/CHANGELOG +2454 -34
  2. data/README +1 -1
  3. data/RUNNING_UNIT_TESTS +3 -34
  4. data/Rakefile +98 -77
  5. data/install.rb +1 -1
  6. data/lib/active_record.rb +13 -22
  7. data/lib/active_record/aggregations.rb +38 -49
  8. data/lib/active_record/associations.rb +452 -333
  9. data/lib/active_record/associations/association_collection.rb +66 -20
  10. data/lib/active_record/associations/association_proxy.rb +9 -8
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -51
  12. data/lib/active_record/associations/has_many_association.rb +21 -57
  13. data/lib/active_record/associations/has_many_through_association.rb +38 -18
  14. data/lib/active_record/associations/has_one_association.rb +30 -14
  15. data/lib/active_record/attribute_methods.rb +253 -0
  16. data/lib/active_record/base.rb +719 -494
  17. data/lib/active_record/calculations.rb +62 -63
  18. data/lib/active_record/callbacks.rb +57 -83
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +38 -9
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  22. data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -12
  23. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +191 -62
  24. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +37 -34
  25. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -17
  26. data/lib/active_record/connection_adapters/mysql_adapter.rb +119 -37
  27. data/lib/active_record/connection_adapters/postgresql_adapter.rb +473 -210
  28. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +91 -107
  30. data/lib/active_record/fixtures.rb +503 -113
  31. data/lib/active_record/locking/optimistic.rb +72 -34
  32. data/lib/active_record/migration.rb +80 -57
  33. data/lib/active_record/observer.rb +13 -10
  34. data/lib/active_record/query_cache.rb +16 -57
  35. data/lib/active_record/reflection.rb +35 -38
  36. data/lib/active_record/schema.rb +5 -5
  37. data/lib/active_record/schema_dumper.rb +35 -13
  38. data/lib/active_record/serialization.rb +98 -0
  39. data/lib/active_record/serializers/json_serializer.rb +71 -0
  40. data/lib/active_record/{xml_serialization.rb → serializers/xml_serializer.rb} +90 -83
  41. data/lib/active_record/timestamp.rb +20 -21
  42. data/lib/active_record/transactions.rb +39 -43
  43. data/lib/active_record/validations.rb +256 -107
  44. data/lib/active_record/version.rb +3 -3
  45. data/lib/activerecord.rb +1 -0
  46. data/test/aaa_create_tables_test.rb +15 -2
  47. data/test/abstract_unit.rb +24 -17
  48. data/test/active_schema_test_mysql.rb +20 -8
  49. data/test/adapter_test.rb +23 -5
  50. data/test/adapter_test_sqlserver.rb +15 -1
  51. data/test/aggregations_test.rb +16 -1
  52. data/test/all.sh +2 -2
  53. data/test/associations/ar_joins_test.rb +0 -0
  54. data/test/associations/callbacks_test.rb +51 -30
  55. data/test/associations/cascaded_eager_loading_test.rb +1 -29
  56. data/test/associations/eager_singularization_test.rb +145 -0
  57. data/test/associations/eager_test.rb +42 -6
  58. data/test/associations/extension_test.rb +6 -1
  59. data/test/associations/inner_join_association_test.rb +88 -0
  60. data/test/associations/join_model_test.rb +47 -16
  61. data/test/associations_test.rb +449 -226
  62. data/test/attribute_methods_test.rb +97 -0
  63. data/test/base_test.rb +251 -105
  64. data/test/binary_test.rb +22 -27
  65. data/test/calculations_test.rb +37 -5
  66. data/test/callbacks_test.rb +23 -0
  67. data/test/connection_test_firebird.rb +2 -2
  68. data/test/connection_test_mysql.rb +30 -0
  69. data/test/connections/native_mysql/connection.rb +3 -0
  70. data/test/connections/native_sqlite/connection.rb +5 -14
  71. data/test/connections/native_sqlite3/connection.rb +5 -14
  72. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -1
  73. data/test/{copy_table_sqlite.rb → copy_table_test_sqlite.rb} +8 -3
  74. data/test/datatype_test_postgresql.rb +178 -27
  75. data/test/{empty_date_time_test.rb → date_time_test.rb} +13 -1
  76. data/test/defaults_test.rb +8 -1
  77. data/test/deprecated_finder_test.rb +7 -128
  78. data/test/finder_test.rb +192 -54
  79. data/test/fixtures/all/developers.yml +0 -0
  80. data/test/fixtures/all/people.csv +0 -0
  81. data/test/fixtures/all/tasks.yml +0 -0
  82. data/test/fixtures/author.rb +12 -5
  83. data/test/fixtures/binaries.yml +130 -435
  84. data/test/fixtures/category.rb +6 -0
  85. data/test/fixtures/company.rb +8 -1
  86. data/test/fixtures/computer.rb +1 -0
  87. data/test/fixtures/contact.rb +16 -0
  88. data/test/fixtures/customer.rb +2 -2
  89. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  90. data/test/fixtures/db_definitions/db2.sql +4 -0
  91. data/test/fixtures/db_definitions/firebird.drop.sql +3 -1
  92. data/test/fixtures/db_definitions/firebird.sql +6 -0
  93. data/test/fixtures/db_definitions/frontbase.drop.sql +1 -0
  94. data/test/fixtures/db_definitions/frontbase.sql +5 -0
  95. data/test/fixtures/db_definitions/openbase.sql +41 -25
  96. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  97. data/test/fixtures/db_definitions/oracle.sql +5 -0
  98. data/test/fixtures/db_definitions/postgresql.drop.sql +7 -0
  99. data/test/fixtures/db_definitions/postgresql.sql +87 -58
  100. data/test/fixtures/db_definitions/postgresql2.sql +1 -2
  101. data/test/fixtures/db_definitions/schema.rb +280 -0
  102. data/test/fixtures/db_definitions/schema2.rb +11 -0
  103. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  104. data/test/fixtures/db_definitions/sqlite.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/sybase.sql +4 -0
  107. data/test/fixtures/developer.rb +10 -0
  108. data/test/fixtures/example.log +1 -0
  109. data/test/fixtures/flowers.jpg +0 -0
  110. data/test/fixtures/item.rb +7 -0
  111. data/test/fixtures/items.yml +4 -0
  112. data/test/fixtures/joke.rb +0 -3
  113. data/test/fixtures/matey.rb +4 -0
  114. data/test/fixtures/mateys.yml +4 -0
  115. data/test/fixtures/minimalistic.rb +2 -0
  116. data/test/fixtures/minimalistics.yml +2 -0
  117. data/test/fixtures/mixins.yml +2 -100
  118. data/test/fixtures/parrot.rb +13 -0
  119. data/test/fixtures/parrots.yml +27 -0
  120. data/test/fixtures/parrots_pirates.yml +7 -0
  121. data/test/fixtures/pirate.rb +5 -0
  122. data/test/fixtures/pirates.yml +9 -0
  123. data/test/fixtures/post.rb +1 -0
  124. data/test/fixtures/project.rb +3 -2
  125. data/test/fixtures/reserved_words/distinct.yml +5 -0
  126. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  127. data/test/fixtures/reserved_words/group.yml +14 -0
  128. data/test/fixtures/reserved_words/select.yml +8 -0
  129. data/test/fixtures/reserved_words/values.yml +7 -0
  130. data/test/fixtures/ship.rb +3 -0
  131. data/test/fixtures/ships.yml +5 -0
  132. data/test/fixtures/tagging.rb +4 -0
  133. data/test/fixtures/taggings.yml +8 -1
  134. data/test/fixtures/topic.rb +13 -1
  135. data/test/fixtures/treasure.rb +4 -0
  136. data/test/fixtures/treasures.yml +10 -0
  137. data/test/fixtures_test.rb +205 -24
  138. data/test/inheritance_test.rb +7 -1
  139. data/test/json_serialization_test.rb +180 -0
  140. data/test/lifecycle_test.rb +1 -1
  141. data/test/locking_test.rb +85 -2
  142. data/test/migration_test.rb +206 -40
  143. data/test/mixin_test.rb +13 -515
  144. data/test/pk_test.rb +3 -6
  145. data/test/query_cache_test.rb +104 -0
  146. data/test/reflection_test.rb +16 -0
  147. data/test/reserved_word_test_mysql.rb +177 -0
  148. data/test/schema_dumper_test.rb +38 -3
  149. data/test/serialization_test.rb +47 -0
  150. data/test/transactions_test.rb +74 -23
  151. data/test/unconnected_test.rb +1 -1
  152. data/test/validations_test.rb +322 -32
  153. data/test/xml_serialization_test.rb +121 -44
  154. metadata +48 -41
  155. data/examples/associations.rb +0 -87
  156. data/examples/shared_setup.rb +0 -15
  157. data/examples/validation.rb +0 -85
  158. data/lib/active_record/acts/list.rb +0 -256
  159. data/lib/active_record/acts/nested_set.rb +0 -211
  160. data/lib/active_record/acts/tree.rb +0 -96
  161. data/lib/active_record/connection_adapters/db2_adapter.rb +0 -228
  162. data/lib/active_record/connection_adapters/firebird_adapter.rb +0 -728
  163. data/lib/active_record/connection_adapters/frontbase_adapter.rb +0 -861
  164. data/lib/active_record/connection_adapters/openbase_adapter.rb +0 -350
  165. data/lib/active_record/connection_adapters/oracle_adapter.rb +0 -690
  166. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +0 -591
  167. data/lib/active_record/connection_adapters/sybase_adapter.rb +0 -662
  168. data/lib/active_record/deprecated_associations.rb +0 -104
  169. data/lib/active_record/deprecated_finders.rb +0 -44
  170. data/lib/active_record/vendor/simple.rb +0 -693
  171. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  172. data/lib/active_record/wrappings.rb +0 -58
  173. data/test/connections/native_sqlserver/connection.rb +0 -23
  174. data/test/connections/native_sqlserver_odbc/connection.rb +0 -25
  175. data/test/deprecated_associations_test.rb +0 -396
  176. data/test/fixtures/db_definitions/mysql.drop.sql +0 -32
  177. data/test/fixtures/db_definitions/mysql.sql +0 -234
  178. data/test/fixtures/db_definitions/mysql2.drop.sql +0 -2
  179. data/test/fixtures/db_definitions/mysql2.sql +0 -5
  180. data/test/fixtures/db_definitions/sqlserver.drop.sql +0 -34
  181. data/test/fixtures/db_definitions/sqlserver.sql +0 -243
  182. data/test/fixtures/db_definitions/sqlserver2.drop.sql +0 -2
  183. data/test/fixtures/db_definitions/sqlserver2.sql +0 -5
  184. data/test/fixtures/mixin.rb +0 -63
  185. data/test/mixin_nested_set_test.rb +0 -196
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  def find(*args)
12
- options = Base.send(:extract_options_from_args!, args)
12
+ options = args.extract_options!
13
13
 
14
14
  conditions = "#{@finder_sql}"
15
15
  if sanitized_conditions = sanitize_sql(options[:conditions])
@@ -51,16 +51,14 @@ module ActiveRecord
51
51
  through = @reflection.through_reflection
52
52
  raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record?
53
53
 
54
- load_target
55
-
56
54
  klass = through.klass
57
55
  klass.transaction do
58
56
  flatten_deeper(records).each do |associate|
59
57
  raise_on_type_mismatch(associate)
60
58
  raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
61
59
 
62
- @owner.send(@reflection.through_reflection.name).proxy_target << klass.with_scope(:create => construct_join_attributes(associate)) { klass.create! }
63
- @target << associate
60
+ @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(associate)) { klass.create! }
61
+ @target << associate if loaded?
64
62
  end
65
63
  end
66
64
 
@@ -69,9 +67,9 @@ module ActiveRecord
69
67
 
70
68
  [:push, :concat].each { |method| alias_method method, :<< }
71
69
 
72
- # Remove +records+ from this association. Does not destroy +records+.
70
+ # Removes +records+ from this association. Does not destroy +records+.
73
71
  def delete(*records)
74
- return if records.empty?
72
+ records = flatten_deeper(records)
75
73
  records.each { |associate| raise_on_type_mismatch(associate) }
76
74
 
77
75
  through = @reflection.through_reflection
@@ -85,7 +83,7 @@ module ActiveRecord
85
83
  raise_on_type_mismatch(associate)
86
84
  raise ActiveRecord::HasManyThroughCantDissociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
87
85
 
88
- @owner.send(@reflection.through_reflection.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))
86
+ @owner.send(through.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))
89
87
  @target.delete(associate)
90
88
  end
91
89
  end
@@ -96,22 +94,33 @@ module ActiveRecord
96
94
  def build(attrs = nil)
97
95
  raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)
98
96
  end
97
+ alias_method :new, :build
99
98
 
100
99
  def create!(attrs = nil)
101
100
  @reflection.klass.transaction do
102
- self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! }
101
+ self << (object = @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! })
102
+ object
103
103
  end
104
104
  end
105
105
 
106
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
107
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
108
+ # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
109
+ def size
110
+ return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
111
+ return @target.size if loaded?
112
+ return count
113
+ end
114
+
106
115
  # Calculate sum using SQL, not Enumerable
107
116
  def sum(*args, &block)
108
117
  calculate(:sum, *args, &block)
109
118
  end
110
119
 
111
120
  def count(*args)
112
- column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
121
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
113
122
  if @reflection.options[:uniq]
114
- # This is needed becase 'SELECT count(DISTINCT *)..' is not valid sql statement.
123
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid sql statement.
115
124
  column_name = "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key}" if column_name == :all
116
125
  options.merge!(:distinct => true)
117
126
  end
@@ -123,7 +132,7 @@ module ActiveRecord
123
132
  if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
124
133
  super
125
134
  else
126
- @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
135
+ @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
127
136
  end
128
137
  end
129
138
 
@@ -154,11 +163,11 @@ module ActiveRecord
154
163
 
155
164
  # Construct attributes for :through pointing to owner and associate.
156
165
  def construct_join_attributes(associate)
157
- returning construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) do |join_attributes|
158
- if @reflection.options[:source_type]
159
- join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
160
- end
166
+ join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
167
+ if @reflection.options[:source_type]
168
+ join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
161
169
  end
170
+ join_attributes
162
171
  end
163
172
 
164
173
  # Associate attributes pointing to owner, quoted.
@@ -226,7 +235,9 @@ module ActiveRecord
226
235
  :find => { :from => construct_from,
227
236
  :conditions => construct_conditions,
228
237
  :joins => construct_joins,
229
- :select => construct_select } }
238
+ :select => construct_select,
239
+ :order => @reflection.options[:order],
240
+ :limit => @reflection.options[:limit] } }
230
241
  end
231
242
 
232
243
  def construct_sql
@@ -253,11 +264,20 @@ module ActiveRecord
253
264
  @conditions ||= [
254
265
  (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
255
266
  (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]),
267
+ (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.source_reflection.options[:conditions])) if @reflection.source_reflection.options[:conditions]),
256
268
  ("#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.name.demodulize)}" unless @reflection.through_reflection.klass.descends_from_active_record?)
257
- ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
269
+ ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && !@reflection.source_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
258
270
  end
259
271
 
260
272
  alias_method :sql_conditions, :conditions
273
+
274
+ def has_cached_counter?
275
+ @owner.attribute_present?(cached_counter_attribute_name)
276
+ end
277
+
278
+ def cached_counter_attribute_name
279
+ "#{@reflection.name}_count"
280
+ end
261
281
  end
262
282
  end
263
283
  end
@@ -6,23 +6,16 @@ module ActiveRecord
6
6
  construct_sql
7
7
  end
8
8
 
9
- def create(attributes = {}, replace_existing = true)
10
- record = build(attributes, replace_existing)
11
- record.save
12
- record
9
+ def create(attrs = {}, replace_existing = true)
10
+ new_record(replace_existing) { |klass| klass.create(attrs) }
13
11
  end
14
12
 
15
- def build(attributes = {}, replace_existing = true)
16
- record = @reflection.klass.new(attributes)
17
-
18
- if replace_existing
19
- replace(record, true)
20
- else
21
- record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
22
- self.target = record
23
- end
13
+ def create!(attrs = {}, replace_existing = true)
14
+ new_record(replace_existing) { |klass| klass.create!(attrs) }
15
+ end
24
16
 
25
- record
17
+ def build(attrs = {}, replace_existing = true)
18
+ new_record(replace_existing) { |klass| klass.new(attrs) }
26
19
  end
27
20
 
28
21
  def replace(obj, dont_save = false)
@@ -75,6 +68,29 @@ module ActiveRecord
75
68
  end
76
69
  @finder_sql << " AND (#{conditions})" if conditions
77
70
  end
71
+
72
+ def construct_scope
73
+ create_scoping = {}
74
+ set_belongs_to_association_for(create_scoping)
75
+ { :create => create_scoping }
76
+ end
77
+
78
+ def new_record(replace_existing)
79
+ # Make sure we load the target first, if we plan on replacing the existing
80
+ # instance. Otherwise, if the target has not previously been loaded
81
+ # elsewhere, the instance we create will get orphaned.
82
+ load_target if replace_existing
83
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
84
+
85
+ if replace_existing
86
+ replace(record, true)
87
+ else
88
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
89
+ self.target = record
90
+ end
91
+
92
+ record
93
+ end
78
94
  end
79
95
  end
80
96
  end
@@ -1,10 +1,13 @@
1
1
  module ActiveRecord
2
2
  module AttributeMethods #:nodoc:
3
3
  DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
4
+ ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
4
5
 
5
6
  def self.included(base)
6
7
  base.extend ClassMethods
7
8
  base.attribute_method_suffix *DEFAULT_SUFFIXES
9
+ base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
10
+ base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
8
11
  end
9
12
 
10
13
  # Declare and check for suffixed attribute methods.
@@ -43,6 +46,68 @@ module ActiveRecord
43
46
  @@attribute_method_regexp.match(method_name)
44
47
  end
45
48
 
49
+
50
+ # Contains the names of the generated attribute methods.
51
+ def generated_methods #:nodoc:
52
+ @generated_methods ||= Set.new
53
+ end
54
+
55
+ def generated_methods?
56
+ !generated_methods.empty?
57
+ end
58
+
59
+ # generates all the attribute related methods for columns in the database
60
+ # accessors, mutators and query methods
61
+ def define_attribute_methods
62
+ return if generated_methods?
63
+ columns_hash.each do |name, column|
64
+ unless instance_method_already_implemented?(name)
65
+ if self.serialized_attributes[name]
66
+ define_read_method_for_serialized_attribute(name)
67
+ else
68
+ define_read_method(name.to_sym, name, column)
69
+ end
70
+ end
71
+
72
+ unless instance_method_already_implemented?("#{name}=")
73
+ define_write_method(name.to_sym)
74
+ end
75
+
76
+ unless instance_method_already_implemented?("#{name}?")
77
+ define_question_method(name)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Check to see if the method is defined in the model or any of its subclasses that also derive from ActiveRecord.
83
+ # Raise DangerousAttributeError if the method is defined by ActiveRecord though.
84
+ def instance_method_already_implemented?(method_name)
85
+ return true if method_name =~ /^id(=$|\?$|$)/
86
+ @_defined_class_methods ||= Set.new(ancestors.first(ancestors.index(ActiveRecord::Base)).collect! { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.flatten)
87
+ @@_defined_activerecord_methods ||= Set.new(ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false))
88
+ raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
89
+ @_defined_class_methods.include?(method_name)
90
+ end
91
+
92
+ alias :define_read_methods :define_attribute_methods
93
+
94
+ # +cache_attributes+ allows you to declare which converted attribute values should
95
+ # be cached. Usually caching only pays off for attributes with expensive conversion
96
+ # methods, like date columns (e.g. created_at, updated_at).
97
+ def cache_attributes(*attribute_names)
98
+ attribute_names.each {|attr| cached_attributes << attr.to_s}
99
+ end
100
+
101
+ # returns the attributes where
102
+ def cached_attributes
103
+ @cached_attributes ||=
104
+ columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
105
+ end
106
+
107
+ def cache_attribute?(attr_name)
108
+ cached_attributes.include?(attr_name)
109
+ end
110
+
46
111
  private
47
112
  # Suffixes a, ?, c become regexp /(a|\?|c)$/
48
113
  def rebuild_attribute_method_regexp
@@ -54,9 +119,197 @@ module ActiveRecord
54
119
  def attribute_method_suffixes
55
120
  @@attribute_method_suffixes ||= []
56
121
  end
122
+
123
+ # Define an attribute reader method. Cope with nil column.
124
+ def define_read_method(symbol, attr_name, column)
125
+ cast_code = column.type_cast_code('v') if column
126
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
127
+
128
+ unless attr_name.to_s == self.primary_key.to_s
129
+ access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
130
+ end
131
+
132
+ if cache_attribute?(attr_name)
133
+ access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
134
+ end
135
+ evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
136
+ end
137
+
138
+ # Define read method for serialized attribute.
139
+ def define_read_method_for_serialized_attribute(attr_name)
140
+ evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
141
+ end
142
+
143
+ # Define an attribute ? method.
144
+ def define_question_method(attr_name)
145
+ evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
146
+ end
147
+
148
+ def define_write_method(attr_name)
149
+ evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
150
+ end
151
+
152
+ # Evaluate the definition for an attribute related method
153
+ def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
154
+
155
+ unless method_name.to_s == primary_key.to_s
156
+ generated_methods << method_name
157
+ end
158
+
159
+ begin
160
+ class_eval(method_definition, __FILE__, __LINE__)
161
+ rescue SyntaxError => err
162
+ generated_methods.delete(attr_name)
163
+ if logger
164
+ logger.warn "Exception occurred during reader method compilation."
165
+ logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
166
+ logger.warn "#{err.message}"
167
+ end
168
+ end
169
+ end
170
+ end # ClassMethods
171
+
172
+
173
+ # Allows access to the object attributes, which are held in the @attributes hash, as though they
174
+ # were first-class methods. So a Person class with a name attribute can use Person#name and
175
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
176
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
177
+ # the completed attribute is not nil or 0.
178
+ #
179
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
180
+ # table with a master_id foreign key can instantiate master through Client#master.
181
+ def method_missing(method_id, *args, &block)
182
+ method_name = method_id.to_s
183
+
184
+ # If we haven't generated any methods yet, generate them, then
185
+ # see if we've created the method we're looking for.
186
+ if !self.class.generated_methods?
187
+ self.class.define_attribute_methods
188
+ if self.class.generated_methods.include?(method_name)
189
+ return self.send(method_id, *args, &block)
190
+ end
191
+ end
192
+
193
+ if self.class.primary_key.to_s == method_name
194
+ id
195
+ elsif md = self.class.match_attribute_method?(method_name)
196
+ attribute_name, method_type = md.pre_match, md.to_s
197
+ if @attributes.include?(attribute_name)
198
+ __send__("attribute#{method_type}", attribute_name, *args, &block)
199
+ else
200
+ super
201
+ end
202
+ elsif @attributes.include?(method_name)
203
+ read_attribute(method_name)
204
+ else
205
+ super
206
+ end
207
+ end
208
+
209
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
210
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
211
+ def read_attribute(attr_name)
212
+ attr_name = attr_name.to_s
213
+ if !(value = @attributes[attr_name]).nil?
214
+ if column = column_for_attribute(attr_name)
215
+ if unserializable_attribute?(attr_name, column)
216
+ unserialize_attribute(attr_name)
217
+ else
218
+ column.type_cast(value)
219
+ end
220
+ else
221
+ value
222
+ end
223
+ else
224
+ nil
225
+ end
226
+ end
227
+
228
+ def read_attribute_before_type_cast(attr_name)
229
+ @attributes[attr_name]
230
+ end
231
+
232
+ # Returns true if the attribute is of a text column and marked for serialization.
233
+ def unserializable_attribute?(attr_name, column)
234
+ column.text? && self.class.serialized_attributes[attr_name]
235
+ end
236
+
237
+ # Returns the unserialized object of the attribute.
238
+ def unserialize_attribute(attr_name)
239
+ unserialized_object = object_from_yaml(@attributes[attr_name])
240
+
241
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
242
+ @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
243
+ else
244
+ raise SerializationTypeMismatch,
245
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
246
+ end
247
+ end
248
+
249
+
250
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
251
+ # columns are turned into nil.
252
+ def write_attribute(attr_name, value)
253
+ attr_name = attr_name.to_s
254
+ @attributes_cache.delete(attr_name)
255
+ if (column = column_for_attribute(attr_name)) && column.number?
256
+ @attributes[attr_name] = convert_number_column_value(value)
257
+ else
258
+ @attributes[attr_name] = value
259
+ end
260
+ end
261
+
262
+
263
+ def query_attribute(attr_name)
264
+ unless value = read_attribute(attr_name)
265
+ false
266
+ else
267
+ column = self.class.columns_hash[attr_name]
268
+ if column.nil?
269
+ if Numeric === value || value !~ /[^0-9]/
270
+ !value.to_i.zero?
271
+ else
272
+ !value.blank?
273
+ end
274
+ elsif column.number?
275
+ !value.zero?
276
+ else
277
+ !value.blank?
278
+ end
279
+ end
57
280
  end
281
+
282
+ # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
283
+ # person.respond_to?("name?") which will all return true.
284
+ alias :respond_to_without_attributes? :respond_to?
285
+ def respond_to?(method, include_priv = false)
286
+ method_name = method.to_s
287
+ if super
288
+ return true
289
+ elsif !self.class.generated_methods?
290
+ self.class.define_attribute_methods
291
+ if self.class.generated_methods.include?(method_name)
292
+ return true
293
+ end
294
+ end
295
+
296
+ if @attributes.nil?
297
+ return super
298
+ elsif @attributes.include?(method_name)
299
+ return true
300
+ elsif md = self.class.match_attribute_method?(method_name)
301
+ return true if @attributes.include?(md.pre_match)
302
+ end
303
+ super
304
+ end
305
+
58
306
 
59
307
  private
308
+
309
+ def missing_attribute(attr_name, stack)
310
+ raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
311
+ end
312
+
60
313
  # Handle *? for method_missing.
61
314
  def attribute?(attribute_name)
62
315
  query_attribute(attribute_name)