activerecord 1.2.0 → 1.3.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 (35) hide show
  1. data/CHANGELOG +65 -0
  2. data/install.rb +2 -2
  3. data/lib/active_record.rb +1 -1
  4. data/lib/active_record/acts/list.rb +13 -13
  5. data/lib/active_record/associations.rb +38 -0
  6. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -1
  7. data/lib/active_record/associations/has_many_association.rb +2 -1
  8. data/lib/active_record/base.rb +44 -20
  9. data/lib/active_record/connection_adapters/abstract_adapter.rb +10 -11
  10. data/lib/active_record/connection_adapters/mysql_adapter.rb +6 -2
  11. data/lib/active_record/connection_adapters/postgresql_adapter.rb +7 -2
  12. data/lib/active_record/connection_adapters/sqlite_adapter.rb +10 -2
  13. data/lib/active_record/fixtures.rb +18 -0
  14. data/lib/active_record/support/binding_of_caller.rb +81 -0
  15. data/lib/active_record/support/breakpoint.rb +525 -0
  16. data/lib/active_record/timestamp.rb +17 -5
  17. data/lib/active_record/transactions.rb +11 -13
  18. data/lib/active_record/validations.rb +31 -13
  19. data/rakefile +1 -1
  20. data/test/abstract_unit.rb +2 -0
  21. data/test/base_test.rb +29 -9
  22. data/test/fixtures/db_definitions/mysql.sql +5 -3
  23. data/test/fixtures/db_definitions/postgresql.sql +2 -0
  24. data/test/fixtures/db_definitions/sqlite.sql +3 -1
  25. data/test/fixtures/db_definitions/sqlserver.sql +2 -0
  26. data/test/fixtures/developer.rb +2 -4
  27. data/test/fixtures/developers.yml +3 -0
  28. data/test/fixtures/fixture_database.sqlite +0 -0
  29. data/test/fixtures/fixture_database_2.sqlite +0 -0
  30. data/test/fixtures/mixin.rb +5 -2
  31. data/test/fixtures/mixins.yml +27 -11
  32. data/test/mixin_test.rb +154 -56
  33. data/test/transactions_test.rb +1 -1
  34. data/test/validations_test.rb +50 -3
  35. metadata +4 -2
data/CHANGELOG CHANGED
@@ -1,3 +1,68 @@
1
+ *1.3.0*
2
+
3
+ * Added a require_association hook on const_missing that makes it possible to use any model class without requiring it first. This makes STI look like:
4
+
5
+ before:
6
+ require_association 'person'
7
+ class Employee < Person
8
+ end
9
+
10
+ after:
11
+ class Employee < Person
12
+ end
13
+
14
+ This also reduces the usefulness of Controller.model in Action Pack to currently only being for documentation purposes.
15
+
16
+ * Added that Base.update_all and Base.delete_all return an integer of the number of affected rows #341
17
+
18
+ * Added scope option to validation_uniqueness #349 [Kent Sibilev]
19
+
20
+ * Added respondence to *_before_type_cast for all attributes to return their string-state before they were type casted by the column type.
21
+ This is helpful for getting "100,000" back on a integer-based validation where the value would normally be "100".
22
+
23
+ * Added allow_nil options to validates_inclusion_of so that validation is only triggered if the attribute is not nil [what-a-day]
24
+
25
+ * Added work-around for PostgreSQL and the problem of getting fixtures to be created from id 1 on each test case.
26
+ This only works for auto-incrementing primary keys called "id" for now #359 [Scott Baron]
27
+
28
+ * Added Base#clear_association_cache to empty all the cached associations #347 [Tobias Luetke]
29
+
30
+ * Added more informative exceptions in establish_connection #356 [bitsweat]
31
+
32
+ * Added Base#update_attributes that'll accept a hash of attributes and save the record (returning true if it passed validation, false otherwise).
33
+
34
+ Before:
35
+ person.attributes = @params["person"]
36
+ person.save
37
+
38
+ Now:
39
+ person.update_attributes(@params["person"])
40
+
41
+ * Added Base.destroy and Base.delete to remove records without holding a reference to them first.
42
+
43
+ * Added that query benchmarking will only happen if its going to be logged anyway #344
44
+
45
+ * Added higher_item and lower_item as public methods for acts_as_list #342 [Tobias Luetke]
46
+
47
+ * Fixed that options[:counter_sql] was overwritten with interpolated sql rather than original sql #355 [bitsweat]
48
+
49
+ * Fixed that overriding an attribute's accessor would be disregarded by add_on_empty and add_on_boundary_breaking because they simply used
50
+ the attributes[] hash instead of checking for @base.respond_to?(attr.to_s). [Marten]
51
+
52
+ * Fixed that Base.table_name would expect a parameter when used in has_and_belongs_to_many joins [Anna Lissa Cruz]
53
+
54
+ * Fixed that nested transactions now work by letting the outer most transaction have the responsibilty of starting and rolling back the transaction.
55
+ If any of the inner transactions swallow the exception raised, though, the transaction will not be rolled back. So always let the transaction
56
+ bubble up even when you've dealt with local issues. Closes #231 and #340.
57
+
58
+ * Fixed validates_{confirmation,acceptance}_of to only happen when the virtual attributes are not nil #348 [dpiddy@gmail.com]
59
+
60
+ * Changed the interface on AbstractAdapter to require that adapters return the number of affected rows on delete and update operations.
61
+
62
+ * Fixed the automated timestamping feature when running under Rails' development environment that resets the inheritable attributes on each request.
63
+
64
+
65
+
1
66
  *1.2.0*
2
67
 
3
68
  * Added Base.validates_inclusion_of that validates whether the value of the specified attribute is available in a particular enumerable
data/install.rb CHANGED
@@ -42,8 +42,8 @@ files = %w-
42
42
  active_record/fixtures.rb
43
43
  active_record/observer.rb
44
44
  active_record/reflection.rb
45
- active_record/mixins/list.rb
46
- active_record/mixins/touch.rb
45
+ active_record/acts/list.rb
46
+ active_record/acts/tree.rb
47
47
  active_record/support/class_attribute_accessors.rb
48
48
  active_record/support/class_inheritable_attributes.rb
49
49
  active_record/support/clean_logger.rb
@@ -41,12 +41,12 @@ require 'active_record/acts/tree'
41
41
 
42
42
  ActiveRecord::Base.class_eval do
43
43
  include ActiveRecord::Validations
44
+ include ActiveRecord::Timestamp
44
45
  include ActiveRecord::Callbacks
45
46
  include ActiveRecord::Associations
46
47
  include ActiveRecord::Aggregations
47
48
  include ActiveRecord::Transactions
48
49
  include ActiveRecord::Reflection
49
- include ActiveRecord::Timestamp
50
50
  include ActiveRecord::Acts::Tree
51
51
  include ActiveRecord::Acts::List
52
52
  end
@@ -109,6 +109,18 @@ module ActiveRecord
109
109
  def last?
110
110
  self.send(position_column) == bottom_position_in_list
111
111
  end
112
+
113
+ def higher_item
114
+ self.class.find_first(
115
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
116
+ )
117
+ end
118
+
119
+ def lower_item
120
+ self.class.find_first(
121
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
122
+ )
123
+ end
112
124
 
113
125
  private
114
126
 
@@ -123,18 +135,6 @@ module ActiveRecord
123
135
  # Overwrite this method to define the scope of the list changes
124
136
  def scope_condition() "1" end
125
137
 
126
- def higher_item
127
- self.class.find_first(
128
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
129
- )
130
- end
131
-
132
- def lower_item
133
- self.class.find_first(
134
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
135
- )
136
- end
137
-
138
138
  def bottom_position_in_list
139
139
  item = bottom_item
140
140
  item ? item.send(position_column) : 0
@@ -163,7 +163,7 @@ module ActiveRecord
163
163
 
164
164
  def increment_positions_on_higher_items
165
165
  self.class.update_all(
166
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column)}"
166
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
167
167
  )
168
168
  end
169
169
 
@@ -3,10 +3,30 @@ require 'active_record/associations/has_many_association'
3
3
  require 'active_record/associations/has_and_belongs_to_many_association'
4
4
  require 'active_record/deprecated_associations'
5
5
 
6
+
6
7
  unless Object.respond_to?(:require_association)
7
8
  Object.send(:define_method, :require_association) { |file_name| ActiveRecord::Base.require_association(file_name) }
8
9
  end
9
10
 
11
+ class Object
12
+ class << self
13
+ # Use const_missing to autoload associations so we don't have to
14
+ # require_association when using single-table inheritance.
15
+ unless respond_to?(:pre_association_const_missing)
16
+ alias_method :pre_association_const_missing, :const_missing
17
+
18
+ def const_missing(class_id)
19
+ begin
20
+ require_association(Inflector.underscore(Inflector.demodulize(class_id.to_s)))
21
+ return Object.const_get(class_id) if Object.const_get(class_id).ancestors.include?(ActiveRecord::Base)
22
+ rescue LoadError
23
+ pre_association_const_missing(class_id)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
10
30
  module ActiveRecord
11
31
  module Associations # :nodoc:
12
32
  def self.append_features(base)
@@ -14,6 +34,13 @@ module ActiveRecord
14
34
  base.extend(ClassMethods)
15
35
  end
16
36
 
37
+ # Clears out the association cache
38
+ def clear_association_cache #:nodoc:
39
+ self.class.reflect_on_all_associations.to_a.each do |assoc|
40
+ instance_variable_set "@#{assoc.name}", nil
41
+ end
42
+ end
43
+
17
44
  # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
18
45
  # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
19
46
  # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
@@ -464,6 +491,17 @@ module ActiveRecord
464
491
  def reset_associations_loaded
465
492
  self.associations_loaded = []
466
493
  end
494
+
495
+ # Reload all the associations that have already been loaded once.
496
+ def reload_associations_loaded
497
+ associations_loaded.each do |file_name|
498
+ begin
499
+ silence_warnings { load("#{file_name}.rb") }
500
+ rescue LoadError
501
+ # The association didn't reside in its own file, so we assume it was required by other means
502
+ end
503
+ end
504
+ end
467
505
 
468
506
  private
469
507
  # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
@@ -5,7 +5,7 @@ module ActiveRecord
5
5
  super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
6
 
7
7
  @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
8
- association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
8
+ association_table_name = options[:table_name] || @association_class.table_name
9
9
  @join_table = join_table
10
10
  @order = options[:order] || "t.#{@association_class.primary_key}"
11
11
 
@@ -14,7 +14,8 @@ module ActiveRecord
14
14
  if options[:counter_sql]
15
15
  @counter_sql = interpolate_sql(options[:counter_sql])
16
16
  elsif options[:finder_sql]
17
- @counter_sql = options[:counter_sql] = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
17
+ options[:counter_sql] = options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
18
+ @counter_sql = interpolate_sql(options[:counter_sql])
18
19
  else
19
20
  @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
20
21
  end
@@ -333,13 +333,23 @@ module ActiveRecord #:nodoc:
333
333
  object
334
334
  end
335
335
 
336
- # Updates all records with the SET-part of an SQL update statement in +updates+. A subset of the records can be selected
337
- # by specifying +conditions+. Example:
336
+ # Deletes the record with the given +id+ without instantiating an object first.
337
+ def delete(id)
338
+ delete_all([ "#{primary_key} = ?", id ])
339
+ end
340
+
341
+ # Destroys the record with the given +id+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
342
+ def destroy(id)
343
+ find(id).destroy
344
+ end
345
+
346
+ # Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updates.
347
+ # A subset of the records can be selected by specifying +conditions+. Example:
338
348
  # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
339
349
  def update_all(updates, conditions = nil)
340
350
  sql = "UPDATE #{table_name} SET #{updates} "
341
351
  add_conditions!(sql, conditions)
342
- connection.update(sql, "#{name} Update")
352
+ return connection.update(sql, "#{name} Update")
343
353
  end
344
354
 
345
355
  # Destroys the objects for all the records that matches the +condition+ by instantiating each object and calling
@@ -463,13 +473,8 @@ module ActiveRecord #:nodoc:
463
473
  # class Mouse < ActiveRecord::Base
464
474
  # def self.table_name() "mice" end
465
475
  # end
466
- def table_name(class_name = nil)
467
- if class_name.nil?
468
- class_name = class_name_of_active_record_descendant(self)
469
- table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
470
- else
471
- table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
472
- end
476
+ def table_name
477
+ table_name_prefix + undecorated_table_name(class_name_of_active_record_descendant(self)) + table_name_suffix
473
478
  end
474
479
 
475
480
  # Defines the primary key field -- can be overridden in subclasses. Overwritting will negate any effect of the
@@ -522,6 +527,7 @@ module ActiveRecord #:nodoc:
522
527
  methods[attr.to_sym] = true
523
528
  methods["#{attr}=".to_sym] = true
524
529
  methods["#{attr}?".to_sym] = true
530
+ methods["#{attr}_before_type_cast".to_sym] = true
525
531
  methods
526
532
  end
527
533
  end
@@ -723,6 +729,10 @@ module ActiveRecord #:nodoc:
723
729
  read_attribute(self.class.primary_key)
724
730
  end
725
731
 
732
+ def id_before_type_cast
733
+ read_attribute_before_type_cast(self.class.primary_key)
734
+ end
735
+
726
736
  def quoted_id
727
737
  quote(id, self.class.columns_hash[self.class.primary_key])
728
738
  end
@@ -777,9 +787,18 @@ module ActiveRecord #:nodoc:
777
787
  end
778
788
 
779
789
  # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
790
+ # Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
791
+ # doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
780
792
  def update_attribute(name, value)
781
793
  self[name] = value
782
- save
794
+ return true
795
+ end
796
+
797
+ # Updates all the attributes in from the passed hash and saves the record. If the object is invalid, the saving will
798
+ # fail and false will be returned.
799
+ def update_attributes(attributes)
800
+ self.attributes = attributes
801
+ return save
783
802
  end
784
803
 
785
804
  # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
@@ -901,10 +920,10 @@ module ActiveRecord #:nodoc:
901
920
  def method_missing(method_id, *arguments)
902
921
  method_name = method_id.id2name
903
922
 
904
-
905
-
906
923
  if method_name =~ read_method? && @attributes.include?($1)
907
924
  return read_attribute($1)
925
+ elsif method_name =~ read_untyped_method? && @attributes.include?($1)
926
+ return read_attribute_before_type_cast($1)
908
927
  elsif method_name =~ write_method? && @attributes.include?($1)
909
928
  write_attribute($1, arguments[0])
910
929
  elsif method_name =~ query_method? && @attributes.include?($1)
@@ -914,25 +933,30 @@ module ActiveRecord #:nodoc:
914
933
  end
915
934
  end
916
935
 
917
- def read_method?() /^([a-zA-Z][-_\w]*)[^=?]*$/ end
918
- def write_method?() /^([a-zA-Z][-_\w]*)=.*$/ end
919
- def query_method?() /^([a-zA-Z][-_\w]*)\?$/ end
936
+ def read_method?() /^([a-zA-Z][-_\w]*)[^=?]*$/ end
937
+ def read_untyped_method?() /^([a-zA-Z][-_\w]*)_before_type_cast$/ end
938
+ def write_method?() /^([a-zA-Z][-_\w]*)=.*$/ end
939
+ def query_method?() /^([a-zA-Z][-_\w]*)\?$/ end
920
940
 
921
- # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
941
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
922
942
  # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
923
943
  def read_attribute(attr_name) #:doc:
924
944
  if @attributes.keys.include? attr_name
925
945
  if column = column_for_attribute(attr_name)
926
- @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
946
+ unserializable_attribute?(attr_name, column) ?
927
947
  unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
948
+ else
949
+ @attributes[attr_name]
928
950
  end
929
-
930
- @attributes[attr_name]
931
951
  else
932
952
  nil
933
953
  end
934
954
  end
935
955
 
956
+ def read_attribute_before_type_cast(attr_name)
957
+ @attributes[attr_name]
958
+ end
959
+
936
960
  # Returns true if the attribute is of a text column and marked for serialization.
937
961
  def unserializable_attribute?(attr_name, column)
938
962
  @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
@@ -86,14 +86,13 @@ module ActiveRecord
86
86
  if configuration = configurations[spec.to_s]
87
87
  establish_connection(configuration)
88
88
  else
89
- raise AdapterNotSpecified
89
+ raise AdapterNotSpecified, "#{spec} database is not configured"
90
90
  end
91
91
  else
92
92
  spec = symbolize_strings_in_hash(spec)
93
- unless spec.key?(:adapter) then raise AdapterNotSpecified end
94
-
93
+ unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
95
94
  adapter_method = "#{spec[:adapter]}_connection"
96
- unless respond_to?(adapter_method) then raise AdapterNotFound end
95
+ unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
97
96
  remove_connection
98
97
  establish_connection(ConnectionSpecification.new(spec, adapter_method))
99
98
  end
@@ -285,10 +284,10 @@ module ActiveRecord
285
284
  # Returns the last auto-generated ID from the affected table.
286
285
  def insert(sql, name = nil, pk = nil, id_value = nil) end
287
286
 
288
- # Executes the update statement.
287
+ # Executes the update statement and returns the number of rows affected.
289
288
  def update(sql, name = nil) end
290
289
 
291
- # Executes the delete statement.
290
+ # Executes the delete statement and returns the number of rows affected.
292
291
  def delete(sql, name = nil) end
293
292
 
294
293
  def reset_runtime # :nodoc:
@@ -298,16 +297,16 @@ module ActiveRecord
298
297
  end
299
298
 
300
299
  # Wrap a block in a transaction. Returns result of block.
301
- def transaction
300
+ def transaction(start_db_transaction = true)
302
301
  begin
303
302
  if block_given?
304
- begin_db_transaction
303
+ begin_db_transaction if start_db_transaction
305
304
  result = yield
306
- commit_db_transaction
305
+ commit_db_transaction if start_db_transaction
307
306
  result
308
307
  end
309
308
  rescue Exception => database_transaction_rollback
310
- rollback_db_transaction
309
+ rollback_db_transaction if start_db_transaction
311
310
  raise
312
311
  end
313
312
  end
@@ -349,7 +348,7 @@ module ActiveRecord
349
348
  protected
350
349
  def log(sql, name, connection, &action)
351
350
  begin
352
- if @logger.nil?
351
+ if @logger.nil? || @logger.level > Logger::INFO
353
352
  action.call(connection)
354
353
  else
355
354
  result = nil
@@ -67,8 +67,12 @@ module ActiveRecord
67
67
  log(sql, name, @connection) { |connection| connection.query(sql) }
68
68
  end
69
69
 
70
- alias_method :update, :execute
71
- alias_method :delete, :execute
70
+ def update(sql, name = nil)
71
+ execute(sql, name)
72
+ @connection.affected_rows
73
+ end
74
+
75
+ alias_method :delete, :update
72
76
 
73
77
  def begin_db_transaction
74
78
  begin
@@ -64,8 +64,13 @@ module ActiveRecord
64
64
  log(sql, name, @connection) { |connection| connection.query(sql) }
65
65
  end
66
66
 
67
- alias_method :update, :execute
68
- alias_method :delete, :execute
67
+ def update(sql, name = nil)
68
+ result = nil
69
+ log(sql, name, @connection) { |connection| result = connection.exec(sql) }
70
+ result.cmdtuples
71
+ end
72
+
73
+ alias_method :delete, :update
69
74
 
70
75
  def begin_db_transaction() execute "BEGIN" end
71
76
  def commit_db_transaction() execute "COMMIT" end
@@ -62,8 +62,16 @@ module ActiveRecord
62
62
  end
63
63
  end
64
64
 
65
- alias_method :update, :execute
66
- alias_method :delete, :execute
65
+ def update(sql, name = nil)
66
+ execute(sql, name)
67
+ @connection.changes
68
+ end
69
+
70
+ def delete(sql, name = nil)
71
+ sql += " WHERE 1=1" unless sql =~ /WHERE/i
72
+ execute(sql, name)
73
+ @connection.changes
74
+ end
67
75
 
68
76
  def begin_db_transaction() execute "BEGIN" end
69
77
  def commit_db_transaction() execute "COMMIT" end