activerecord 1.15.3 → 1.15.4

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.

data/CHANGELOG CHANGED
@@ -1,3 +1,32 @@
1
+ *1.15.4* (October 4th, 2007)
2
+
3
+ * Fix #count on a has_many :through association so that it recognizes the :uniq option. Closes #8801 [lifofifo]
4
+
5
+ * Don't clobber includes passed to has_many.count [danger]
6
+
7
+ * Make sure has_many uses :include when counting [danger]
8
+
9
+ * Save associated records only if the association is already loaded. #8713 [blaine]
10
+
11
+ * Changing the :default Date format doesn't break date quoting. #6312 [bshand, Elias]
12
+
13
+ * Allow nil serialized attributes with a set class constraint. #7293 [sandofsky]
14
+
15
+ * belongs_to assignment creates a new proxy rather than modifying its target in-place. #8412 [mmangino@elevatedrails.com]
16
+
17
+ * Fix column type detection while loading fixtures. Closes #7987 [roderickvd]
18
+
19
+ * Document deep eager includes. #6267 [Josh Susser, Dan Manges]
20
+
21
+ * Oracle: extract column length for CHAR also. #7866 [ymendel]
22
+
23
+ * Small additions and fixes for ActiveRecord documentation. Closes #7342 [jeremymcanally]
24
+
25
+ * SQLite: binary escaping works with $KCODE='u'. #7862 [tsuka]
26
+
27
+ * Improved cloning performance by relying less on exception raising #8159 [Blaine]
28
+
29
+
1
30
  *1.15.3* (March 12th, 2007)
2
31
 
3
32
  * Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool]
data/Rakefile CHANGED
@@ -151,7 +151,7 @@ spec = Gem::Specification.new do |s|
151
151
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
152
152
  end
153
153
 
154
- s.add_dependency('activesupport', '= 1.4.2' + PKG_BUILD)
154
+ s.add_dependency('activesupport', '= 1.4.3' + PKG_BUILD)
155
155
 
156
156
  s.files.delete "test/fixtures/fixture_database.sqlite"
157
157
  s.files.delete "test/fixtures/fixture_database_2.sqlite"
@@ -74,6 +74,7 @@ module ActiveRecord
74
74
  # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
75
75
  # the first in the list of all chapters.
76
76
  module InstanceMethods
77
+ # Insert the item at the given position (defaults to the top position of 1).
77
78
  def insert_at(position = 1)
78
79
  insert_at_position(position)
79
80
  end
@@ -118,6 +119,7 @@ module ActiveRecord
118
119
  end
119
120
  end
120
121
 
122
+ # Removes the item from the list.
121
123
  def remove_from_list
122
124
  decrement_positions_on_lower_items if in_list?
123
125
  end
@@ -162,6 +164,7 @@ module ActiveRecord
162
164
  )
163
165
  end
164
166
 
167
+ # Test if this record is in a list
165
168
  def in_list?
166
169
  !send(position_column).nil?
167
170
  end
@@ -178,21 +181,26 @@ module ActiveRecord
178
181
  # Overwrite this method to define the scope of the list changes
179
182
  def scope_condition() "1" end
180
183
 
184
+ # Returns the bottom position number in the list.
185
+ # bottom_position_in_list # => 2
181
186
  def bottom_position_in_list(except = nil)
182
187
  item = bottom_item(except)
183
188
  item ? item.send(position_column) : 0
184
189
  end
185
190
 
191
+ # Returns the bottom item
186
192
  def bottom_item(except = nil)
187
193
  conditions = scope_condition
188
194
  conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
189
195
  acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
190
196
  end
191
197
 
198
+ # Forces item to assume the bottom position in the list.
192
199
  def assume_bottom_position
193
200
  update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
194
201
  end
195
202
 
203
+ # Forces item to assume the top position in the list.
196
204
  def assume_top_position
197
205
  update_attribute(position_column, 1)
198
206
  end
@@ -227,6 +235,7 @@ module ActiveRecord
227
235
  )
228
236
  end
229
237
 
238
+ # Increments position (<tt>position_column</tt>) of all items in the list.
230
239
  def increment_positions_on_all_items
231
240
  acts_as_list_class.update_all(
232
241
  "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
@@ -70,16 +70,23 @@ module ActiveRecord
70
70
  nodes
71
71
  end
72
72
 
73
+ # Returns the root node of the tree.
73
74
  def root
74
75
  node = self
75
76
  node = node.parent while node.parent
76
77
  node
77
78
  end
78
79
 
80
+ # Returns all siblings of the current node.
81
+ #
82
+ # subchild1.siblings # => [subchild2]
79
83
  def siblings
80
84
  self_and_siblings - [self]
81
85
  end
82
86
 
87
+ # Returns all siblings and a reference to the current node.
88
+ #
89
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
83
90
  def self_and_siblings
84
91
  parent ? parent.children : self.class.roots
85
92
  end
@@ -352,7 +352,15 @@ module ActiveRecord
352
352
  # for post in Post.find(:all, :include => [ :author, :comments ])
353
353
  #
354
354
  # That'll add another join along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id. And we'll be down to 1 query.
355
- # But that shouldn't fool you to think that you can pull out huge amounts of data with no performance penalty just because you've reduced
355
+ #
356
+ # To include a deep hierarchy of associations, using a hash:
357
+ #
358
+ # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
359
+ #
360
+ # That'll grab not only all the comments but all their authors and gravatar pictures. You can mix and match
361
+ # symbols, arrays and hashes in any combination to describe the associations you want to load.
362
+ #
363
+ # All of this power shouldn't fool you into thinking that you can pull out huge amounts of data with no performance penalty just because you've reduced
356
364
  # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
357
365
  # catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
358
366
  #
@@ -734,6 +742,7 @@ module ActiveRecord
734
742
  deprecated_association_comparison_method(reflection.name, reflection.class_name)
735
743
  end
736
744
 
745
+ # Create the callbacks to update counter cache
737
746
  if options[:counter_cache]
738
747
  cache_column = options[:counter_cache] == true ?
739
748
  "#{self.to_s.underscore.pluralize}_count" :
@@ -871,6 +880,12 @@ module ActiveRecord
871
880
  end
872
881
 
873
882
  private
883
+ # Generate a join table name from two provided tables names.
884
+ # The order of names in join name is determined by lexical precedence.
885
+ # join_table_name("members", "clubs")
886
+ # => "clubs_members"
887
+ # join_table_name("members", "special_clubs")
888
+ # => "members_special_clubs"
874
889
  def join_table_name(first_table_name, second_table_name)
875
890
  if first_table_name < second_table_name
876
891
  join_table = "#{first_table_name}_#{second_table_name}"
@@ -880,7 +895,7 @@ module ActiveRecord
880
895
 
881
896
  table_name_prefix + join_table + table_name_suffix
882
897
  end
883
-
898
+
884
899
  def association_accessor_methods(reflection, association_proxy_class)
885
900
  define_method(reflection.name) do |*params|
886
901
  force_reload = params.first unless params.empty?
@@ -901,7 +916,7 @@ module ActiveRecord
901
916
 
902
917
  define_method("#{reflection.name}=") do |new_value|
903
918
  association = instance_variable_get("@#{reflection.name}")
904
- if association.nil?
919
+ if association.nil? || association.target != new_value
905
920
  association = association_proxy_class.new(self, reflection)
906
921
  end
907
922
 
@@ -911,10 +926,7 @@ module ActiveRecord
911
926
  instance_variable_set("@#{reflection.name}", association)
912
927
  else
913
928
  instance_variable_set("@#{reflection.name}", nil)
914
- return nil
915
929
  end
916
-
917
- association
918
930
  end
919
931
 
920
932
  define_method("set_#{reflection.name}_target") do |target|
@@ -981,8 +993,8 @@ module ActiveRecord
981
993
 
982
994
  after_callback = <<-end_eval
983
995
  association = instance_variable_get("@#{association_name}")
984
-
985
- if association.respond_to?(:loaded?)
996
+
997
+ if association.respond_to?(:loaded?) && association.loaded?
986
998
  if @new_record_before_save
987
999
  records_to_save = association
988
1000
  else
@@ -992,7 +1004,7 @@ module ActiveRecord
992
1004
  association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
993
1005
  end
994
1006
  end_eval
995
-
1007
+
996
1008
  # Doesn't use after_save as that would save associations added in after_create/after_update twice
997
1009
  after_create(after_callback)
998
1010
  after_update(after_callback)
@@ -50,7 +50,7 @@ module ActiveRecord
50
50
  options[:conditions] = options[:conditions].nil? ?
51
51
  @finder_sql :
52
52
  @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
53
- options[:include] = @reflection.options[:include]
53
+ options[:include] ||= @reflection.options[:include]
54
54
 
55
55
  @reflection.klass.count(column_name, options)
56
56
  end
@@ -138,7 +138,7 @@ module ActiveRecord
138
138
  elsif @reflection.options[:counter_sql]
139
139
  @reflection.klass.count_by_sql(@counter_sql)
140
140
  else
141
- @reflection.klass.count(:conditions => @counter_sql)
141
+ @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
142
142
  end
143
143
 
144
144
  @target = [] and loaded if count == 0
@@ -101,6 +101,16 @@ module ActiveRecord
101
101
  def sum(*args, &block)
102
102
  calculate(:sum, *args, &block)
103
103
  end
104
+
105
+ def count(*args)
106
+ column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
107
+ if @reflection.options[:uniq]
108
+ # This is needed becase 'SELECT count(DISTINCT *)..' is not valid sql statement.
109
+ column_name = "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key}" if column_name == :all
110
+ options.merge!(:distinct => true)
111
+ end
112
+ @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
113
+ end
104
114
 
105
115
  protected
106
116
  def method_missing(method, *args, &block)
@@ -575,7 +575,7 @@ module ActiveRecord #:nodoc:
575
575
 
576
576
  # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
577
577
  # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
578
- # object must be of that class on retrieval or +SerializationTypeMismatch+ will be raised.
578
+ # object must be of that class on retrieval, or nil. Otherwise, +SerializationTypeMismatch+ will be raised.
579
579
  def serialize(attr_name, class_name = Object)
580
580
  serialized_attributes[attr_name.to_s] = class_name
581
581
  end
@@ -1188,6 +1188,9 @@ module ActiveRecord #:nodoc:
1188
1188
  #
1189
1189
  # It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
1190
1190
  # is actually find_all_by_amount(amount, options).
1191
+ #
1192
+ # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
1193
+ # or find_or_create_by_user_and_password(user, password).
1191
1194
  def method_missing(method_id, *arguments)
1192
1195
  if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
1193
1196
  finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
@@ -1957,7 +1960,7 @@ module ActiveRecord #:nodoc:
1957
1960
  def unserialize_attribute(attr_name)
1958
1961
  unserialized_object = object_from_yaml(@attributes[attr_name])
1959
1962
 
1960
- if unserialized_object.is_a?(self.class.serialized_attributes[attr_name])
1963
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
1961
1964
  @attributes[attr_name] = unserialized_object
1962
1965
  else
1963
1966
  raise SerializationTypeMismatch,
@@ -2156,7 +2159,13 @@ module ActiveRecord #:nodoc:
2156
2159
 
2157
2160
  def clone_attribute_value(reader_method, attribute_name)
2158
2161
  value = send(reader_method, attribute_name)
2159
- value.clone
2162
+
2163
+ case value
2164
+ when nil, Fixnum, true, false
2165
+ value
2166
+ else
2167
+ value.clone
2168
+ end
2160
2169
  rescue TypeError, NoMethodError
2161
2170
  value
2162
2171
  end
@@ -242,8 +242,8 @@ module ActiveRecord
242
242
  options.assert_valid_keys(CALCULATIONS_OPTIONS)
243
243
  end
244
244
 
245
- # converts a given key to the value that the database adapter returns as
246
- #
245
+ # Converts a given key to the value that the database adapter returns as
246
+ # as a usable column name.
247
247
  # users.id #=> users_id
248
248
  # sum(id) #=> sum_id
249
249
  # count(distinct users.id) #=> count_distinct_users_id
@@ -24,7 +24,7 @@ module ActiveRecord
24
24
  when Float, Fixnum, Bignum then value.to_s
25
25
  # BigDecimals need to be output in a non-normalized form and quoted.
26
26
  when BigDecimal then value.to_s('F')
27
- when Date then "'#{value.to_s}'"
27
+ when Date then "'#{value.to_s(:db)}'"
28
28
  when Time, DateTime then "'#{quoted_date(value)}'"
29
29
  else "'#{quote_string(value.to_yaml)}'"
30
30
  end
@@ -320,6 +320,7 @@ begin
320
320
  decode(data_type, 'NUMBER', data_precision,
321
321
  'FLOAT', data_precision,
322
322
  'VARCHAR2', data_length,
323
+ 'CHAR', data_length,
323
324
  null) as limit,
324
325
  decode(data_type, 'NUMBER', data_scale, null) as scale
325
326
  from all_tab_columns
@@ -68,7 +68,7 @@ module ActiveRecord
68
68
  class SQLiteColumn < Column #:nodoc:
69
69
  class << self
70
70
  def string_to_binary(value)
71
- value.gsub(/\0|\%/) do |b|
71
+ value.gsub(/\0|\%/n) do |b|
72
72
  case b
73
73
  when "\0" then "%00"
74
74
  when "%" then "%25"
@@ -77,7 +77,7 @@ module ActiveRecord
77
77
  end
78
78
 
79
79
  def binary_to_string(value)
80
- value.gsub(/%00|%25/) do |b|
80
+ value.gsub(/%00|%25/n) do |b|
81
81
  case b
82
82
  when "%00" then "\0"
83
83
  when "%25" then "%"
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  class Base
3
3
  class << self
4
- # This method is deprecated in favor of find with the :conditions option.
4
+ # DEPRECATION NOTICE: This method is deprecated in favor of find with the :conditions option.
5
5
  #
6
6
  # Works like find, but the record matching +id+ must also meet the +conditions+.
7
7
  # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
@@ -12,7 +12,7 @@ module ActiveRecord
12
12
  end
13
13
  deprecate :find_on_conditions => "use find(ids, :conditions => conditions)"
14
14
 
15
- # This method is deprecated in favor of find(:first, options).
15
+ # DEPRECATION NOTICE: This method is deprecated in favor of find(:first, options).
16
16
  #
17
17
  # Returns the object for the first record responding to the conditions in +conditions+,
18
18
  # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
@@ -24,7 +24,7 @@ module ActiveRecord
24
24
  end
25
25
  deprecate :find_first => "use find(:first, ...)"
26
26
 
27
- # This method is deprecated in favor of find(:all, options).
27
+ # DEPRECATION NOTICE: This method is deprecated in favor of find(:all, options).
28
28
  #
29
29
  # Returns an array of all the objects that could be instantiated from the associated
30
30
  # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
@@ -412,7 +412,7 @@ class Fixture #:nodoc:
412
412
  klass = @class_name.constantize rescue nil
413
413
 
414
414
  list = @fixture.inject([]) do |fixtures, (key, value)|
415
- col = klass.columns_hash[key] if klass.kind_of?(ActiveRecord::Base)
415
+ col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
416
416
  fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
417
417
  end
418
418
  list * ', '
@@ -5,15 +5,6 @@ module ActiveRecord
5
5
  # Timestamping can be turned off by setting
6
6
  # <tt>ActiveRecord::Base.record_timestamps = false</tt>
7
7
  #
8
- # Keep in mind that, via inheritance, you can turn off timestamps on a per
9
- # model basis by setting <tt>record_timestamps</tt> to false in the desired
10
- # models.
11
- #
12
- # class Feed < ActiveRecord::Base
13
- # self.record_timestamps = false
14
- # # ...
15
- # end
16
- #
17
8
  # Timestamps are in the local timezone by default but can use UTC by setting
18
9
  # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
19
10
  module Timestamp
@@ -2,7 +2,7 @@ module ActiveRecord
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
4
  MINOR = 15
5
- TINY = 3
5
+ TINY = 4
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -168,6 +168,12 @@ class EagerAssociationTest < Test::Unit::TestCase
168
168
  posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'")
169
169
  assert_equal 0, posts.size
170
170
  end
171
+
172
+ def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
173
+ author = authors(:david)
174
+ author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
175
+ assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
176
+ end
171
177
 
172
178
  def test_eager_with_has_and_belongs_to_many_and_limit
173
179
  posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
@@ -271,6 +277,13 @@ class EagerAssociationTest < Test::Unit::TestCase
271
277
  assert_not_nil f.account
272
278
  assert_equal companies(:first_firm, :reload).account, f.account
273
279
  end
280
+
281
+ def test_eager_with_multi_table_conditional_properly_counts_the_records_when_using_size
282
+ author = authors(:david)
283
+ posts_with_no_comments = author.posts.select { |post| post.comments.blank? }
284
+ assert_equal posts_with_no_comments.size, author.posts_with_no_comments.size
285
+ assert_equal posts_with_no_comments, author.posts_with_no_comments
286
+ end
274
287
 
275
288
  def test_eager_with_invalid_association_reference
276
289
  assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
@@ -29,7 +29,16 @@ class AssociationsJoinModelTest < Test::Unit::TestCase
29
29
  assert_equal 2, authors(:mary).categorized_posts.size
30
30
  assert_equal 1, authors(:mary).unique_categorized_posts.size
31
31
  end
32
-
32
+
33
+ def test_has_many_uniq_through_count
34
+ author = authors(:mary)
35
+ assert !authors(:mary).unique_categorized_posts.loaded?
36
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
37
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title, {}) }
38
+ assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, { :conditions => "title is NULL" }) }
39
+ assert !authors(:mary).unique_categorized_posts.loaded?
40
+ end
41
+
33
42
  def test_polymorphic_has_many
34
43
  assert posts(:welcome).taggings.include?(taggings(:welcome_general))
35
44
  end