activerecord 1.4.0 → 1.5.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 (55) hide show
  1. data/CHANGELOG +98 -0
  2. data/install.rb +1 -0
  3. data/lib/active_record.rb +1 -0
  4. data/lib/active_record/acts/list.rb +19 -16
  5. data/lib/active_record/associations.rb +164 -164
  6. data/lib/active_record/associations/association_collection.rb +44 -71
  7. data/lib/active_record/associations/association_proxy.rb +76 -0
  8. data/lib/active_record/associations/belongs_to_association.rb +74 -0
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +34 -21
  10. data/lib/active_record/associations/has_many_association.rb +34 -30
  11. data/lib/active_record/associations/has_one_association.rb +48 -0
  12. data/lib/active_record/base.rb +62 -18
  13. data/lib/active_record/callbacks.rb +17 -8
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +11 -10
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -1
  17. data/lib/active_record/connection_adapters/sqlite_adapter.rb +94 -73
  18. data/lib/active_record/deprecated_associations.rb +46 -8
  19. data/lib/active_record/fixtures.rb +1 -1
  20. data/lib/active_record/observer.rb +5 -1
  21. data/lib/active_record/support/binding_of_caller.rb +72 -68
  22. data/lib/active_record/support/breakpoint.rb +526 -524
  23. data/lib/active_record/support/class_inheritable_attributes.rb +105 -29
  24. data/lib/active_record/support/core_ext.rb +1 -0
  25. data/lib/active_record/support/core_ext/hash.rb +5 -0
  26. data/lib/active_record/support/core_ext/hash/keys.rb +35 -0
  27. data/lib/active_record/support/core_ext/numeric.rb +7 -0
  28. data/lib/active_record/support/core_ext/numeric/bytes.rb +33 -0
  29. data/lib/active_record/support/core_ext/numeric/time.rb +59 -0
  30. data/lib/active_record/support/core_ext/string.rb +5 -0
  31. data/lib/active_record/support/core_ext/string/inflections.rb +41 -0
  32. data/lib/active_record/support/dependencies.rb +1 -14
  33. data/lib/active_record/support/inflector.rb +6 -6
  34. data/lib/active_record/support/misc.rb +0 -24
  35. data/lib/active_record/validations.rb +34 -1
  36. data/lib/active_record/vendor/mysql411.rb +305 -0
  37. data/rakefile +11 -2
  38. data/test/abstract_unit.rb +1 -2
  39. data/test/associations_test.rb +234 -23
  40. data/test/base_test.rb +50 -1
  41. data/test/callbacks_test.rb +16 -0
  42. data/test/connections/native_mysql/connection.rb +2 -2
  43. data/test/connections/native_sqlite3/connection.rb +34 -0
  44. data/test/deprecated_associations_test.rb +36 -2
  45. data/test/fixtures/company.rb +2 -0
  46. data/test/fixtures/computer.rb +3 -0
  47. data/test/fixtures/computers.yml +3 -0
  48. data/test/fixtures/db_definitions/db2.sql +5 -0
  49. data/test/fixtures/db_definitions/mysql.sql +5 -0
  50. data/test/fixtures/db_definitions/postgresql.sql +5 -0
  51. data/test/fixtures/db_definitions/sqlite.sql +5 -0
  52. data/test/fixtures/db_definitions/sqlserver.sql +5 -1
  53. data/test/fixtures/fixture_database.sqlite +0 -0
  54. data/test/validations_test.rb +21 -0
  55. metadata +22 -2
@@ -2,37 +2,17 @@ module ActiveRecord
2
2
  module Associations
3
3
  class HasManyAssociation < AssociationCollection #:nodoc:
4
4
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
+ super
6
6
  @conditions = sanitize_sql(options[:conditions])
7
7
 
8
- if options[:finder_sql]
9
- @finder_sql = interpolate_sql(options[:finder_sql])
10
- else
11
- @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
12
- @finder_sql << " AND #{@conditions}" if @conditions
13
- end
14
-
15
- if options[:counter_sql]
16
- @counter_sql = interpolate_sql(options[:counter_sql])
17
- elsif options[:finder_sql]
18
- options[:counter_sql] = options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
19
- @counter_sql = interpolate_sql(options[:counter_sql])
20
- else
21
- @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
22
- end
23
- end
24
-
25
- def create(attributes = {})
26
- # Can't use Base.create since the foreign key may be a protected attribute.
27
- record = build(attributes)
28
- record.save
29
- @collection << record if loaded?
30
- record
8
+ construct_sql
31
9
  end
32
10
 
33
11
  def build(attributes = {})
12
+ load_target
34
13
  record = @association_class.new(attributes)
35
- record[@association_class_primary_key_name] = @owner.id
14
+ record[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
15
+ @target << record
36
16
  record
37
17
  end
38
18
 
@@ -77,10 +57,10 @@ module ActiveRecord
77
57
  elsif @options[:finder_sql]
78
58
  if ids.size == 1
79
59
  id = ids.first
80
- record = load_collection.detect { |record| id == record.id }
60
+ record = load_target.detect { |record| id == record.id }
81
61
  expects_array? ? [record] : record
82
62
  else
83
- load_collection.select { |record| ids.include?(record.id) }
63
+ load_target.select { |record| ids.include?(record.id) }
84
64
  end
85
65
 
86
66
  # Otherwise, delegate to association class with conditions.
@@ -94,12 +74,12 @@ module ActiveRecord
94
74
  # method calls may be chained.
95
75
  def clear
96
76
  @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = #{@owner.quoted_id}")
97
- @collection = []
77
+ @target = []
98
78
  self
99
79
  end
100
80
 
101
81
  protected
102
- def find_all_records
82
+ def find_target
103
83
  find_all
104
84
  end
105
85
 
@@ -122,7 +102,8 @@ module ActiveRecord
122
102
  end
123
103
 
124
104
  def insert_record(record)
125
- record.update_attribute(@association_class_primary_key_name, @owner.id)
105
+ record[@association_class_primary_key_name] = @owner.id
106
+ record.save
126
107
  end
127
108
 
128
109
  def delete_records(records)
@@ -132,6 +113,29 @@ module ActiveRecord
132
113
  "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
133
114
  )
134
115
  end
116
+
117
+ def target_obsolete?
118
+ false
119
+ end
120
+
121
+ def construct_sql
122
+ if @options[:finder_sql]
123
+ @finder_sql = interpolate_sql(@options[:finder_sql])
124
+ else
125
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
126
+ @finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
127
+ end
128
+
129
+ if @options[:counter_sql]
130
+ @counter_sql = interpolate_sql(@options[:counter_sql])
131
+ elsif @options[:finder_sql]
132
+ @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
133
+ @counter_sql = interpolate_sql(@options[:counter_sql])
134
+ else
135
+ @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
136
+ @counter_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
137
+ end
138
+ end
135
139
  end
136
140
  end
137
141
  end
@@ -0,0 +1,48 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasOneAssociation < BelongsToAssociation #:nodoc:
4
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
+ super
6
+
7
+ construct_sql
8
+ end
9
+
10
+ def replace(obj, dont_save = false)
11
+ load_target
12
+ unless @target.nil?
13
+ @target[@association_class_primary_key_name] = nil
14
+ @target.save unless @owner.new_record?
15
+ end
16
+
17
+ if obj.nil?
18
+ @target = nil
19
+ else
20
+ raise_on_type_mismatch(obj)
21
+
22
+ obj[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
23
+ @target = obj
24
+ end
25
+
26
+ @loaded = true
27
+ unless @owner.new_record? or obj.nil? or dont_save
28
+ return (obj.save ? obj : false)
29
+ else
30
+ return obj
31
+ end
32
+ end
33
+
34
+ private
35
+ def find_target
36
+ @association_class.find_first(@finder_sql, @options[:order])
37
+ end
38
+
39
+ def target_obsolete?
40
+ false
41
+ end
42
+
43
+ def construct_sql
44
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@options[:conditions] ? " AND " + @options[:conditions] : ""}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -789,7 +789,6 @@ module ActiveRecord #:nodoc:
789
789
  # * A record does exist: Updates the record with values matching those of the object attributes.
790
790
  def save
791
791
  create_or_update
792
- return true
793
792
  end
794
793
 
795
794
  # Deletes the record in the database and freezes this instance to reflect that no changes should
@@ -808,17 +807,7 @@ module ActiveRecord #:nodoc:
808
807
 
809
808
  # Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
810
809
  def clone
811
- attr = Hash.new
812
-
813
- self.attribute_names.each do |name|
814
- begin
815
- attr[name] = read_attribute(name).clone
816
- rescue TypeError
817
- attr[name] = read_attribute(name)
818
- end
819
- end
820
-
821
- cloned_record = self.class.new(attr)
810
+ cloned_record = self.class.new(self.attributes)
822
811
  cloned_record.instance_variable_set "@new_record", true
823
812
  cloned_record.id = nil
824
813
  cloned_record
@@ -829,7 +818,7 @@ module ActiveRecord #:nodoc:
829
818
  # doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
830
819
  def update_attribute(name, value)
831
820
  self[name] = value
832
- return true
821
+ save
833
822
  end
834
823
 
835
824
  # Updates all the attributes in from the passed hash and saves the record. If the object is invalid, the saving will
@@ -839,17 +828,59 @@ module ActiveRecord #:nodoc:
839
828
  return save
840
829
  end
841
830
 
831
+ # Initializes the +attribute+ to zero if nil and adds one. Only makes sense for number-based attributes. Returns self.
832
+ def increment(attribute)
833
+ self[attribute] ||= 0
834
+ self[attribute] += 1
835
+ self
836
+ end
837
+
838
+ # Increments the +attribute+ and saves the record.
839
+ def increment!(attribute)
840
+ increment(attribute).update_attribute(attribute, self[attribute])
841
+ end
842
+
843
+ # Initializes the +attribute+ to zero if nil and subtracts one. Only makes sense for number-based attributes. Returns self.
844
+ def decrement(attribute)
845
+ self[attribute] ||= 0
846
+ self[attribute] -= 1
847
+ self
848
+ end
849
+
850
+ # Decrements the +attribute+ and saves the record.
851
+ def decrement!(attribute)
852
+ decrement(attribute).update_attribute(attribute, self[attribute])
853
+ end
854
+
855
+ # Turns an +attribute+ that's currently true into false and vice versa. Returns self.
856
+ def toggle(attribute)
857
+ self[attribute] = quote(!send("#{attribute}?", column_for_attribute(attribute)))
858
+ self
859
+ end
860
+
861
+ # Toggles the +attribute+ and saves the record.
862
+ def toggle!(attribute)
863
+ toggle(attribute).update_attribute(attribute, self[attribute])
864
+ end
865
+
866
+ # Reloads the attributes of this object from the database.
867
+ def reload
868
+ clear_association_cache
869
+ @attributes.update(self.class.find(self.id).instance_variable_get('@attributes'))
870
+ return self
871
+ end
872
+
842
873
  # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
843
874
  # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
844
875
  # (Alias for the protected read_attribute method).
845
876
  def [](attr_name)
846
- read_attribute(attr_name)
877
+ read_attribute(attr_name.to_s)
847
878
  end
848
879
 
849
880
  # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
850
881
  # (Alias for the protected write_attribute method).
851
882
  def []= (attr_name, value)
852
- write_attribute(attr_name, value)
883
+ write_attribute(attr_name.to_s, value)
853
884
  end
854
885
 
855
886
  # Allows you to set all the attributes at once by passing in a hash with keys
@@ -867,6 +898,18 @@ module ActiveRecord #:nodoc:
867
898
  assign_multiparameter_attributes(multi_parameter_attributes)
868
899
  end
869
900
 
901
+ # Returns a hash of all the attributes with their names as keys and clones of their objects as values.
902
+ def attributes
903
+ self.attribute_names.inject({}) do |attributes, name|
904
+ begin
905
+ attributes[name] = read_attribute(name).clone
906
+ rescue TypeError
907
+ attributes[name] = read_attribute(name)
908
+ end
909
+ attributes
910
+ end
911
+ end
912
+
870
913
  # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
871
914
  # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings).
872
915
  def attribute_present?(attribute)
@@ -881,12 +924,12 @@ module ActiveRecord #:nodoc:
881
924
 
882
925
  # Returns the column object for the named attribute.
883
926
  def column_for_attribute(name)
884
- self.class.columns_hash[name]
927
+ self.class.columns_hash[name.to_s]
885
928
  end
886
929
 
887
- # Returns true if the +comparison_object+ is of the same type and has the same id.
930
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
888
931
  def ==(comparison_object)
889
- comparison_object.instance_of?(self.class) && comparison_object.id == id
932
+ comparison_object.equal?(self) or (comparison_object.instance_of?(self.class) and comparison_object.id == id)
890
933
  end
891
934
 
892
935
  # Delegates to ==
@@ -912,6 +955,7 @@ module ActiveRecord #:nodoc:
912
955
  private
913
956
  def create_or_update
914
957
  if new_record? then create else update end
958
+ return true
915
959
  end
916
960
 
917
961
  # Updates the associated record with values matching those of the instant attributes.
@@ -153,6 +153,12 @@ module ActiveRecord
153
153
  # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
154
154
  # after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
155
155
  # after_initialize won't work.
156
+ #
157
+ # == Cancelling callbacks
158
+ #
159
+ # If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns
160
+ # false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
161
+ # defined as methods on the model, which are called last.
156
162
  module Callbacks
157
163
  CALLBACKS = %w(
158
164
  after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
@@ -227,7 +233,7 @@ module ActiveRecord
227
233
  # Is called _after_ Base.save (regardless of whether it's a create or update save).
228
234
  def after_save() end
229
235
  def create_or_update_with_callbacks #:nodoc:
230
- callback(:before_save)
236
+ return false if callback(:before_save) == false
231
237
  result = create_or_update_without_callbacks
232
238
  callback(:after_save)
233
239
  result
@@ -239,7 +245,7 @@ module ActiveRecord
239
245
  # Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
240
246
  def after_create() end
241
247
  def create_with_callbacks #:nodoc:
242
- callback(:before_create)
248
+ return false if callback(:before_create) == false
243
249
  result = create_without_callbacks
244
250
  callback(:after_create)
245
251
  result
@@ -252,7 +258,7 @@ module ActiveRecord
252
258
  def after_update() end
253
259
 
254
260
  def update_with_callbacks #:nodoc:
255
- callback(:before_update)
261
+ return false if callback(:before_update) == false
256
262
  result = update_without_callbacks
257
263
  callback(:after_update)
258
264
  result
@@ -281,8 +287,9 @@ module ActiveRecord
281
287
  def after_validation_on_update() end
282
288
 
283
289
  def valid_with_callbacks #:nodoc:
284
- callback(:before_validation)
285
- if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
290
+ return false if callback(:before_validation) == false
291
+ if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
292
+ return false if result == false
286
293
 
287
294
  result = valid_without_callbacks
288
295
 
@@ -298,7 +305,7 @@ module ActiveRecord
298
305
  # Is called _after_ Base.destroy (and all the attributes have been frozen).
299
306
  def after_destroy() end
300
307
  def destroy_with_callbacks #:nodoc:
301
- callback(:before_destroy)
308
+ return false if callback(:before_destroy) == false
302
309
  result = destroy_without_callbacks
303
310
  callback(:after_destroy)
304
311
  result
@@ -307,7 +314,7 @@ module ActiveRecord
307
314
  private
308
315
  def callback(method)
309
316
  callbacks_for(method).each do |callback|
310
- case callback
317
+ result = case callback
311
318
  when Symbol
312
319
  self.send(callback)
313
320
  when String
@@ -321,9 +328,11 @@ module ActiveRecord
321
328
  raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
322
329
  end
323
330
  end
331
+ return false if result == false
324
332
  end
325
333
 
326
334
  invoke_and_notify(method)
335
+ true
327
336
  end
328
337
 
329
338
  def callbacks_for(method)
@@ -340,4 +349,4 @@ module ActiveRecord
340
349
  self.class.notify_observers(method, self)
341
350
  end
342
351
  end
343
- end
352
+ end
@@ -89,7 +89,7 @@ module ActiveRecord
89
89
  raise AdapterNotSpecified, "#{spec} database is not configured"
90
90
  end
91
91
  else
92
- spec = symbolize_strings_in_hash(spec)
92
+ spec = spec.symbolize_keys
93
93
  unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
94
94
  adapter_method = "#{spec[:adapter]}_connection"
95
95
  unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
@@ -152,10 +152,7 @@ module ActiveRecord
152
152
 
153
153
  # Converts all strings in a hash to symbols.
154
154
  def self.symbolize_strings_in_hash(hash)
155
- hash.inject({}) do |hash_with_symbolized_strings, pair|
156
- hash_with_symbolized_strings[pair.first.to_sym] = pair.last
157
- hash_with_symbolized_strings
158
- end
155
+ hash.symbolize_keys
159
156
  end
160
157
  end
161
158
 
@@ -356,7 +353,7 @@ module ActiveRecord
356
353
  end
357
354
 
358
355
  def quote_column_name(name)
359
- return name
356
+ name
360
357
  end
361
358
 
362
359
  # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
@@ -367,16 +364,20 @@ module ActiveRecord
367
364
  end
368
365
 
369
366
  protected
370
- def log(sql, name, connection, &action)
367
+ def log(sql, name, connection = nil)
368
+ connection ||= @connection
371
369
  begin
372
370
  if @logger.nil? || @logger.level > Logger::INFO
373
- action.call(connection)
374
- else
371
+ yield connection
372
+ elsif block_given?
375
373
  result = nil
376
- bm = measure { result = action.call(connection) }
374
+ bm = measure { result = yield connection }
377
375
  @runtime += bm.real
378
376
  log_info(sql, name, bm.real)
379
377
  result
378
+ else
379
+ log_info(sql, name, 0)
380
+ nil
380
381
  end
381
382
  rescue => e
382
383
  log_info("#{e.message}: #{sql}", name, 0)
@@ -13,6 +13,7 @@ module ActiveRecord
13
13
  # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
14
14
  begin
15
15
  require 'active_record/vendor/mysql'
16
+ require 'active_record/vendor/mysql411'
16
17
  rescue LoadError
17
18
  raise cannot_require_mysql
18
19
  end
@@ -37,6 +37,7 @@ module ActiveRecord
37
37
  end
38
38
 
39
39
  module ConnectionAdapters
40
+
40
41
  class PostgreSQLAdapter < AbstractAdapter # :nodoc:
41
42
  def select_all(sql, name = nil)
42
43
  select(sql, name)
@@ -76,6 +77,14 @@ module ActiveRecord
76
77
  def commit_db_transaction() execute "COMMIT" end
77
78
  def rollback_db_transaction() execute "ROLLBACK" end
78
79
 
80
+ def quote(value, column = nil)
81
+ if value.class == String && column && column.type == :binary
82
+ quote_bytea(value)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
79
88
  def quote_column_name(name)
80
89
  return "\"#{name}\""
81
90
  end
@@ -96,13 +105,31 @@ module ActiveRecord
96
105
  fields = res.fields
97
106
  results.each do |row|
98
107
  hashed_row = {}
99
- row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] }
108
+ row.each_index do |cel_index|
109
+ column = row[cel_index]
110
+ if res.type(cel_index) == 17 # type oid for bytea
111
+ column = unescape_bytea(column)
112
+ end
113
+ hashed_row[fields[cel_index]] = column
114
+ end
100
115
  rows << hashed_row
101
116
  end
102
117
  end
103
118
  return rows
104
119
  end
105
120
 
121
+ def quote_bytea(s)
122
+ "'#{escape_bytea(s)}'"
123
+ end
124
+
125
+ def escape_bytea(s)
126
+ s.gsub(/\\/) { '\\\\\\\\' }.gsub(/[^\\]/) { |c| sprintf('\\\\%03o', c[0].to_i) }
127
+ end
128
+
129
+ def unescape_bytea(s)
130
+ s.gsub(/\\([0-9][0-9][0-9])/) { $1.oct.chr }.gsub(/\\\\/) { '\\' }
131
+ end
132
+
106
133
  def split_table_schema(table_name)
107
134
  schema_split = table_name.split('.')
108
135
  schema_name = "public"
@@ -141,6 +168,7 @@ module ActiveRecord
141
168
  when 'numeric', 'real', 'money' then 'float'
142
169
  when 'character varying', 'interval' then 'string'
143
170
  when 'timestamp without time zone' then 'datetime'
171
+ when 'bytea' then 'binary'
144
172
  else field_type
145
173
  end
146
174