activerecord 1.9.1 → 1.10.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 (68) hide show
  1. data/CHANGELOG +78 -0
  2. data/README +1 -1
  3. data/install.rb +7 -42
  4. data/lib/active_record.rb +2 -0
  5. data/lib/active_record/acts/list.rb +28 -4
  6. data/lib/active_record/acts/nested_set.rb +212 -0
  7. data/lib/active_record/associations.rb +203 -21
  8. data/lib/active_record/associations/association_proxy.rb +10 -2
  9. data/lib/active_record/associations/belongs_to_association.rb +0 -1
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +15 -9
  11. data/lib/active_record/associations/has_many_association.rb +25 -25
  12. data/lib/active_record/associations/has_one_association.rb +2 -2
  13. data/lib/active_record/base.rb +134 -110
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +9 -9
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +4 -0
  16. data/lib/active_record/connection_adapters/oci_adapter.rb +2 -2
  17. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1 -2
  18. data/lib/active_record/deprecated_associations.rb +1 -19
  19. data/lib/active_record/deprecated_finders.rb +41 -0
  20. data/lib/active_record/fixtures.rb +24 -11
  21. data/lib/active_record/observer.rb +17 -11
  22. data/lib/active_record/reflection.rb +5 -1
  23. data/lib/active_record/transactions.rb +7 -0
  24. data/lib/active_record/validations.rb +32 -33
  25. data/rakefile +30 -6
  26. data/test/associations_go_eager_test.rb +55 -0
  27. data/test/associations_test.rb +72 -15
  28. data/test/base_test.rb +15 -21
  29. data/test/deprecated_associations_test.rb +0 -24
  30. data/test/deprecated_finder_test.rb +147 -0
  31. data/test/finder_test.rb +37 -37
  32. data/test/fixtures/author.rb +3 -0
  33. data/test/fixtures/authors.yml +7 -0
  34. data/test/fixtures/categories.yml +7 -0
  35. data/test/fixtures/categories_posts.yml +11 -0
  36. data/test/fixtures/category.rb +3 -0
  37. data/test/fixtures/comment.rb +5 -0
  38. data/test/fixtures/comments.yml +17 -0
  39. data/test/fixtures/company.rb +3 -0
  40. data/test/fixtures/courses.yml +4 -4
  41. data/test/fixtures/db_definitions/db2.drop.sql +6 -0
  42. data/test/fixtures/db_definitions/db2.sql +46 -0
  43. data/test/fixtures/db_definitions/mysql.drop.sql +6 -1
  44. data/test/fixtures/db_definitions/mysql.sql +60 -12
  45. data/test/fixtures/db_definitions/mysql2.sql +1 -1
  46. data/test/fixtures/db_definitions/oci.drop.sql +5 -0
  47. data/test/fixtures/db_definitions/oci.sql +45 -0
  48. data/test/fixtures/db_definitions/postgresql.drop.sql +6 -0
  49. data/test/fixtures/db_definitions/postgresql.sql +45 -0
  50. data/test/fixtures/db_definitions/sqlite.drop.sql +6 -1
  51. data/test/fixtures/db_definitions/sqlite.sql +46 -0
  52. data/test/fixtures/db_definitions/sqlserver.drop.sql +7 -1
  53. data/test/fixtures/db_definitions/sqlserver.sql +46 -0
  54. data/test/fixtures/fk_test_has_fk.yml +3 -0
  55. data/test/fixtures/fk_test_has_pk.yml +2 -0
  56. data/test/fixtures/mixin.rb +18 -0
  57. data/test/fixtures/mixins.yml +30 -0
  58. data/test/fixtures/post.rb +8 -0
  59. data/test/fixtures/posts.yml +20 -0
  60. data/test/fixtures/task.rb +3 -0
  61. data/test/fixtures/tasks.yml +7 -0
  62. data/test/fixtures_test.rb +34 -2
  63. data/test/mixin_nested_set_test.rb +184 -0
  64. data/test/mixin_test.rb +28 -3
  65. data/test/validations_test.rb +16 -0
  66. metadata +21 -5
  67. data/test/fixtures/db_definitions/drop_oracle_tables.sql +0 -35
  68. data/test/fixtures/db_definitions/drop_oracle_tables2.sql +0 -3
@@ -25,13 +25,21 @@ module ActiveRecord
25
25
  end
26
26
 
27
27
  def respond_to?(symbol, include_priv = false)
28
- load_target
29
- proxy_respond_to?(symbol, include_priv) || @target.respond_to?(symbol, include_priv)
28
+ proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
30
29
  end
31
30
 
32
31
  def loaded?
33
32
  @loaded
34
33
  end
34
+
35
+ def target
36
+ @target
37
+ end
38
+
39
+ def target=(t)
40
+ @target = t
41
+ @loaded = true
42
+ end
35
43
 
36
44
  protected
37
45
  def dependent?
@@ -1,7 +1,6 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
3
  class BelongsToAssociation < AssociationProxy #:nodoc:
4
-
5
4
  def reset
6
5
  @target = nil
7
6
  @loaded = false
@@ -7,7 +7,7 @@ module ActiveRecord
7
7
  @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
8
8
  @association_table_name = options[:table_name] || @association_class.table_name
9
9
  @join_table = options[:join_table]
10
- @order = options[:order] || "t.#{@association_class.primary_key}"
10
+ @order = options[:order]
11
11
 
12
12
  construct_sql
13
13
  end
@@ -51,7 +51,7 @@ module ActiveRecord
51
51
 
52
52
  # If no block is given, raise RecordNotFound.
53
53
  if ids.empty?
54
- raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID#{conditions}"
54
+ raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID"
55
55
 
56
56
  # If using a custom finder_sql, scan the entire collection.
57
57
  elsif @options[:finder_sql]
@@ -66,7 +66,7 @@ module ActiveRecord
66
66
  # Otherwise, construct a query.
67
67
  else
68
68
  ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
69
- records = find_target(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
69
+ records = find_target(@finder_sql.sub(/(ORDER BY|$)/, "AND j.#{@association_foreign_key} IN (#{ids_list}) \\1"))
70
70
  if records.size == ids.size
71
71
  if ids.size == 1 and !expects_array
72
72
  records.first
@@ -146,12 +146,18 @@ module ActiveRecord
146
146
 
147
147
  def construct_sql
148
148
  interpolate_sql_options!(@options, :finder_sql, :delete_sql)
149
- @finder_sql = @options[:finder_sql] ||
150
- "SELECT t.*, j.* FROM #{@join_table} j, #{@association_table_name} t " +
151
- "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
152
- "j.#{@association_class_primary_key_name} = #{@owner.quoted_id} " +
153
- (@options[:conditions] ? " AND " + interpolate_sql(@options[:conditions]) : "") + " " +
154
- "ORDER BY #{@order}"
149
+
150
+ if @options[:finder_sql]
151
+ @finder_sql = @options[:finder_sql]
152
+ else
153
+ @finder_sql =
154
+ "SELECT t.*, j.* FROM #{@join_table} j, #{@association_table_name} t " +
155
+ "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
156
+ "j.#{@association_class_primary_key_name} = #{@owner.quoted_id} "
157
+
158
+ @finder_sql << " AND #{interpolate_sql(@options[:conditions])}" if @options[:conditions]
159
+ @finder_sql << " ORDER BY #{@order}" if @order
160
+ end
155
161
  end
156
162
  end
157
163
  end
@@ -20,6 +20,7 @@ module ActiveRecord
20
20
  end
21
21
  end
22
22
 
23
+ # DEPRECATED.
23
24
  def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
24
25
  if @options[:finder_sql]
25
26
  records = @association_class.find_by_sql(@finder_sql)
@@ -31,6 +32,11 @@ module ActiveRecord
31
32
  end
32
33
  end
33
34
 
35
+ # DEPRECATED. Find the first associated record. All arguments are optional.
36
+ def find_first(conditions = nil, orderings = nil)
37
+ find_all(conditions, orderings, 1).first
38
+ end
39
+
34
40
  # Count the number of associated records. All arguments are optional.
35
41
  def count(runtime_conditions = nil)
36
42
  if @options[:counter_sql]
@@ -43,24 +49,15 @@ module ActiveRecord
43
49
  @association_class.count(sql)
44
50
  end
45
51
  end
46
-
47
- # Find the first associated record. All arguments are optional.
48
- def find_first(conditions = nil, orderings = nil)
49
- find_all(conditions, orderings, 1).first
50
- end
51
52
 
52
53
  def find(*args)
53
- # Return an Array if multiple ids are given.
54
- expects_array = args.first.kind_of?(Array)
55
-
56
- ids = args.flatten.compact.uniq
57
-
58
- # If no ids given, raise RecordNotFound.
59
- if ids.empty?
60
- raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID"
54
+ options = Base.send(:extract_options_from_args!, args)
61
55
 
62
56
  # If using a custom finder_sql, scan the entire collection.
63
- elsif @options[:finder_sql]
57
+ if @options[:finder_sql]
58
+ expects_array = args.first.kind_of?(Array)
59
+ ids = args.flatten.compact.uniq
60
+
64
61
  if ids.size == 1
65
62
  id = ids.first
66
63
  record = load_target.detect { |record| id == record.id }
@@ -68,11 +65,10 @@ module ActiveRecord
68
65
  else
69
66
  load_target.select { |record| ids.include?(record.id) }
70
67
  end
71
-
72
- # Otherwise, delegate to association class with conditions.
73
68
  else
74
- args << { :conditions => "#{@association_class_primary_key_name} = #{@owner.quoted_id} #{@conditions ? " AND " + @conditions : ""}" }
75
- @association_class.find(*args)
69
+ options[:conditions] = @finder_sql + (options[:conditions] ? " AND #{options[:conditions]}" : "")
70
+ options[:order] = options[:order] ? "#{options[:order]}, #{@options[:order]}" : @options[:order]
71
+ @association_class.find(args.size == 1 ? args.first : args, options)
76
72
  end
77
73
  end
78
74
 
@@ -113,11 +109,15 @@ module ActiveRecord
113
109
  end
114
110
 
115
111
  def delete_records(records)
116
- ids = quoted_record_ids(records)
117
- @association_class.update_all(
118
- "#{@association_class_primary_key_name} = NULL",
119
- "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
120
- )
112
+ if @options[:dependent]
113
+ records.each { |r| r.destroy }
114
+ else
115
+ ids = quoted_record_ids(records)
116
+ @association_class.update_all(
117
+ "#{@association_class_primary_key_name} = NULL",
118
+ "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
119
+ )
120
+ end
121
121
  end
122
122
 
123
123
  def target_obsolete?
@@ -128,7 +128,7 @@ module ActiveRecord
128
128
  if @options[:finder_sql]
129
129
  @finder_sql = interpolate_sql(@options[:finder_sql])
130
130
  else
131
- @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
131
+ @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}"
132
132
  @finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
133
133
  end
134
134
 
@@ -138,7 +138,7 @@ module ActiveRecord
138
138
  @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
139
139
  @counter_sql = interpolate_sql(@options[:counter_sql])
140
140
  else
141
- @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
141
+ @counter_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}"
142
142
  @counter_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
143
143
  end
144
144
  end
@@ -38,7 +38,7 @@ module ActiveRecord
38
38
 
39
39
  private
40
40
  def find_target
41
- @association_class.find_first(@finder_sql, @options[:order])
41
+ @association_class.find(:first, :conditions => @finder_sql, :order => @options[:order])
42
42
  end
43
43
 
44
44
  def target_obsolete?
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
  end
47
47
 
48
48
  def construct_sql
49
- @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@options[:conditions] ? " AND " + @options[:conditions] : ""}"
49
+ @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@options[:conditions] ? " AND " + @options[:conditions] : ""}"
50
50
  end
51
51
  end
52
52
  end
@@ -1,4 +1,5 @@
1
1
  require 'yaml'
2
+ require 'active_record/deprecated_finders'
2
3
 
3
4
  module ActiveRecord #:nodoc:
4
5
  class ActiveRecordError < StandardError #:nodoc:
@@ -54,7 +55,7 @@ module ActiveRecord #:nodoc:
54
55
  # Active Records accepts constructor parameters either in a hash or as a block. The hash method is especially useful when
55
56
  # you're receiving the data from somewhere else, like a HTTP request. It works like this:
56
57
  #
57
- # user = User.new("name" => "David", "occupation" => "Code Artist")
58
+ # user = User.new(:name => "David", :occupation => "Code Artist")
58
59
  # user.name # => "David"
59
60
  #
60
61
  # You can also use block initialization:
@@ -95,7 +96,7 @@ module ActiveRecord #:nodoc:
95
96
  # question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
96
97
  # the question marks with symbols and supplying a hash with values for the matching symbol keys:
97
98
  #
98
- # Company.find_first([
99
+ # Company.find(:first, [
99
100
  # "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
100
101
  # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
101
102
  # ])
@@ -111,14 +112,17 @@ module ActiveRecord #:nodoc:
111
112
  # # Uses an integer of seconds to hold the length of the song
112
113
  #
113
114
  # def length=(minutes)
114
- # write_attribute("length", minutes * 60)
115
+ # write_attribute(:length, minutes * 60)
115
116
  # end
116
117
  #
117
118
  # def length
118
- # read_attribute("length") / 60
119
+ # read_attribute(:length) / 60
119
120
  # end
120
121
  # end
121
122
  #
123
+ # You can alternatively use self[:attribute]=(value) and self[:attribute] instead of write_attribute(:attribute, vaule) and
124
+ # read_attribute(:attribute) as a shorter form.
125
+ #
122
126
  # == Accessing attributes before they have been type casted
123
127
  #
124
128
  # Some times you want to be able to read the raw attribute data without having the column-determined type cast run its course first.
@@ -133,16 +137,16 @@ module ActiveRecord #:nodoc:
133
137
  #
134
138
  # Dynamic attribute-based finders are a cleaner way of getting objects by simple queries without turning to SQL. They work by
135
139
  # appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name, Payment.find_by_transaction_id</tt>.
136
- # So instead of writing <tt>Person.find_first(["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
140
+ # So instead of writing <tt>Person.find(:first, ["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
137
141
  #
138
142
  # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
139
143
  # <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
140
- # <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do
144
+ # <tt>Person.find(:first, ["user_name = ? AND password = ?", user_name, password])</tt>, you just do
141
145
  # <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
142
146
  #
143
- # It's even possible to use all the additional parameters to find_first and find_all. For example, the full interface for Payment.find_all_by_amount
144
- # is actually Payment.find_all_by_amount(amount, orderings = nil, limit = nil, joins = nil). And the full interface to Person.find_by_user_name is
145
- # actually Person.find_by_user_name(user_name, orderings = nil)
147
+ # It's even possible to use all the additional parameters to find. For example, the full interface for Payment.find_all_by_amount
148
+ # is actually Payment.find_all_by_amount(amount, options). And the full interface to Person.find_by_user_name is
149
+ # actually Person.find_by_user_name(user_name, options). So you could call <tt>Payment.find_all_by_amount(50, :order => "created_on")</tt>.
146
150
  #
147
151
  # == Saving arrays, hashes, and other non-mappable objects in text columns
148
152
  #
@@ -153,7 +157,7 @@ module ActiveRecord #:nodoc:
153
157
  # serialize :preferences
154
158
  # end
155
159
  #
156
- # user = User.create("preferences" => { "background" => "black", "display" => large })
160
+ # user = User.create(:preferences) => { "background" => "black", "display" => large })
157
161
  # User.find(user.id).preferences # => { "background" => "black", "display" => large }
158
162
  #
159
163
  # You can also specify an class option as the second parameter that'll raise an exception if a serialized object is retrieved as a
@@ -163,7 +167,7 @@ module ActiveRecord #:nodoc:
163
167
  # serialize :preferences, Hash
164
168
  # end
165
169
  #
166
- # user = User.create("preferences" => %w( one two three ))
170
+ # user = User.create(:preferences => %w( one two three ))
167
171
  # User.find(user.id).preferences # raises SerializationTypeMismatch
168
172
  #
169
173
  # == Single table inheritance
@@ -176,8 +180,8 @@ module ActiveRecord #:nodoc:
176
180
  # class Client < Company; end
177
181
  # class PriorityClient < Client; end
178
182
  #
179
- # When you do Firm.create("name" => "37signals"), this record will be saved in the companies table with type = "Firm". You can then
180
- # fetch this row again using Company.find_first "name = '37signals'" and it will return a Firm object.
183
+ # When you do Firm.create(:name => "37signals"), this record will be saved in the companies table with type = "Firm". You can then
184
+ # fetch this row again using Company.find(:first, "name = '37signals'") and it will return a Firm object.
181
185
  #
182
186
  # If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just
183
187
  # like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
@@ -285,93 +289,76 @@ module ActiveRecord #:nodoc:
285
289
  @@default_timezone = :local
286
290
 
287
291
  class << self # Class methods
288
- # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
289
- # If only one ID is specified, that object is returned directly. If more than one ID is specified, an array is returned.
290
- # Examples:
292
+ # Find operates with three different retreval approaches:
293
+ #
294
+ # * Find by id: This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
295
+ # If no record can be found for all of the listed ids, then RecordNotFound will be raised.
296
+ # * Find first: This will return the first record matched by the options used. These options can either be specific
297
+ # conditions or merely an order. If no record can matched, nil is returned.
298
+ # * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
299
+ #
300
+ # All approaches accepts an option hash as their last parameter. The options are:
301
+ #
302
+ # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
303
+ # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name".
304
+ # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
305
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
306
+ # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
307
+ # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
308
+ # to already defined associations. See eager loading under Associations.
309
+ #
310
+ # Examples for find by id:
291
311
  # Person.find(1) # returns the object for ID = 1
292
312
  # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
293
313
  # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
294
314
  # Person.find([1]) # returns an array for objects the object with ID = 1
315
+ # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
295
316
  #
296
- # The last argument may be a Hash of find options. Currently, +conditions+ is the only option, behaving the same as with +find_all+.
297
- # Person.find(1, :conditions => "associate_id = 5"
298
- # Person.find(1, 2, 6, :conditions => "status = 'active'"
299
- # Person.find([7, 17], :conditions => ["sanitize_me = ?", "bare'quote"]
300
- # Person.find(25, :conditions => ["name = :name AND age = :age", { :name => "Mary", :age => 22 }]
317
+ # Examples for find first:
318
+ # Person.find(:first) # returns the first object fetched by SELECT * FROM people
319
+ # Person.find(:first, :conditions => [ "user_name = ?", user_name])
320
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
301
321
  #
302
- # +RecordNotFound+ is raised if no record can be found.
322
+ # Examples for find all:
323
+ # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
324
+ # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
325
+ # Person.find(:all, :offset => 10, :limit => 10)
326
+ # Person.find(:all, :include => [ :account, :friends ])
303
327
  def find(*args)
304
- # Return an Array if ids are passed in an Array.
305
- expects_array = args.first.kind_of?(Array)
306
-
307
- # Extract options hash from argument list.
308
328
  options = extract_options_from_args!(args)
309
- conditions = " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
310
-
311
- ids = args.flatten.compact.uniq
312
- case ids.size
313
-
314
- # Raise if no ids passed.
315
- when 0
316
- raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
317
-
318
- # Find a single id.
319
- when 1
320
- unless result = find_first("#{primary_key} = #{sanitize(ids.first)}#{conditions}")
321
- raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
322
- end
323
-
324
- # Box result if expecting array.
325
- expects_array ? [result] : result
326
329
 
327
- # Find multiple ids.
330
+ case args.first
331
+ when :first
332
+ find(:all, options.merge({ :limit => 1 })).first
333
+ when :all
334
+ options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options))
328
335
  else
329
- ids_list = ids.map { |id| sanitize(id) }.join(',')
330
- result = find_all("#{primary_key} IN (#{ids_list})#{conditions}", primary_key)
331
- if result.size == ids.size
332
- result
333
- else
334
- raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
336
+ expects_array = args.first.kind_of?(Array)
337
+ conditions = " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
338
+
339
+ ids = args.flatten.compact.uniq
340
+ case ids.size
341
+ when 0
342
+ raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
343
+ when 1
344
+ if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" }))
345
+ return expects_array ? [ result ] : result
346
+ else
347
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
348
+ end
349
+ else
350
+ # Find multiple ids
351
+ ids_list = ids.map { |id| sanitize(id) }.join(',')
352
+ result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}", :order => primary_key }))
353
+ if result.size == ids.size
354
+ return result
355
+ else
356
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
357
+ end
335
358
  end
336
359
  end
337
360
  end
338
361
 
339
- # Returns true if the given +id+ represents the primary key of a record in the database, false otherwise.
340
- # Example:
341
- # Person.exists?(5)
342
- def exists?(id)
343
- !find_first("#{primary_key} = #{sanitize(id)}").nil? rescue false
344
- end
345
-
346
- # This method is deprecated in favor of find with the :conditions option.
347
- # Works like find, but the record matching +id+ must also meet the +conditions+.
348
- # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
349
- # Example:
350
- # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
351
- def find_on_conditions(ids, conditions)
352
- find(ids, :conditions => conditions)
353
- end
354
-
355
- # Returns an array of all the objects that could be instantiated from the associated
356
- # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
357
- # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
358
- # such as by "last_name, first_name DESC". A maximum of returned objects and their offset can be specified in
359
- # +limit+ with either just a single integer as the limit or as an array with the first element as the limit,
360
- # the second as the offset. Examples:
361
- # Project.find_all "category = 'accounts'", "last_accessed DESC", 15
362
- # Project.find_all ["category = ?", category_name], "created ASC", [15, 20]
363
- def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
364
- sql = "SELECT * FROM #{table_name} "
365
- sql << "#{joins} " if joins
366
- add_conditions!(sql, conditions)
367
- sql << "ORDER BY #{orderings} " unless orderings.nil?
368
-
369
- limit = sanitize_sql(limit) if limit.is_a? Array and limit.first.is_a? String
370
- connection.add_limit!(sql, limit) if limit
371
-
372
- find_by_sql(sql)
373
- end
374
-
375
362
  # Works like find_all, but requires a complete SQL string. Examples:
376
363
  # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
377
364
  # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
@@ -379,15 +366,13 @@ module ActiveRecord #:nodoc:
379
366
  connection.select_all(sanitize_sql(sql), "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
380
367
  end
381
368
 
382
- # Returns the object for the first record responding to the conditions in +conditions+,
383
- # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
384
- # be used to create the object. In such cases, it might be beneficial to also specify
385
- # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
386
- # Employee.find_first "income > 50000", "income DESC, name"
387
- def find_first(conditions = nil, orderings = nil)
388
- find_all(conditions, orderings, 1).first
369
+ # Returns true if the given +id+ represents the primary key of a record in the database, false otherwise.
370
+ # Example:
371
+ # Person.exists?(5)
372
+ def exists?(id)
373
+ !find(:first, :conditions => ["#{primary_key} = ?", id]).nil? rescue false
389
374
  end
390
-
375
+
391
376
  # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
392
377
  # fail under validations, the unsaved object is still returned.
393
378
  def create(attributes = nil)
@@ -452,8 +437,10 @@ module ActiveRecord #:nodoc:
452
437
 
453
438
  # Returns the number of records that meets the +conditions+. Zero is returned if no records match. Example:
454
439
  # Product.count "sales > 1"
455
- def count(conditions = nil)
456
- sql = "SELECT COUNT(*) FROM #{table_name} "
440
+ def count(conditions = nil, joins = nil)
441
+ tbl_var_name = joins ? table_name[0,1].downcase : ""
442
+ sql = "SELECT COUNT(*) FROM #{table_name} #{tbl_var_name} "
443
+ sql << ", #{joins} " if joins
457
444
  add_conditions!(sql, conditions)
458
445
  count_by_sql(sql)
459
446
  end
@@ -462,8 +449,14 @@ module ActiveRecord #:nodoc:
462
449
  # Product.count "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
463
450
  def count_by_sql(sql)
464
451
  sql = sanitize_conditions(sql)
465
- count = connection.select_one(sql, "#{name} Count").values.first
466
- return count ? count.to_i : 0
452
+ rows = connection.select_one(sql, "#{name} Count")
453
+
454
+ if rows.nil?
455
+ return 0
456
+ else
457
+ count = rows.values.first
458
+ return count ? count.to_i : 0
459
+ end
467
460
  end
468
461
 
469
462
  # Increments the specified counter by one. So <tt>DiscussionBoard.increment_counter("post_count",
@@ -625,6 +618,10 @@ module ActiveRecord #:nodoc:
625
618
  def columns_hash
626
619
  @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
627
620
  end
621
+
622
+ def column_names
623
+ @column_names ||= columns_hash.keys
624
+ end
628
625
 
629
626
  # Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
630
627
  # and columns used for single table inheritance has been removed.
@@ -647,7 +644,7 @@ module ActiveRecord #:nodoc:
647
644
 
648
645
  # Resets all the cached information about columns, which will cause they to be reloaded on the next request.
649
646
  def reset_column_information
650
- @columns = @columns_hash = @content_columns = @dynamic_methods_hash = nil
647
+ @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = nil
651
648
  end
652
649
 
653
650
  def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@@ -685,16 +682,17 @@ module ActiveRecord #:nodoc:
685
682
  def benchmark(title)
686
683
  result = nil
687
684
  bm = Benchmark.measure { result = silence { yield } }
688
- logger.info "#{title} (#{sprintf("%f", bm.real)})"
685
+ logger.info "#{title} (#{sprintf("%f", bm.real)})" if logger
689
686
  return result
690
687
  end
691
688
 
692
689
  # Silences the logger for the duration of the block.
693
690
  def silence
694
691
  result = nil
695
- logger.level = Logger::ERROR
692
+ old_logger_level = logger.level if logger
693
+ logger.level = Logger::ERROR if logger
696
694
  result = yield
697
- logger.level = Logger::DEBUG
695
+ logger.level = old_logger_level if logger
698
696
  return result
699
697
  end
700
698
 
@@ -737,6 +735,24 @@ module ActiveRecord #:nodoc:
737
735
  self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
738
736
  end
739
737
 
738
+ def construct_finder_sql(options)
739
+ sql = "SELECT * FROM #{table_name} "
740
+ sql << "#{options[:joins]} " if options[:joins]
741
+ add_conditions!(sql, options[:conditions])
742
+ sql << "ORDER BY #{options[:order]} " if options[:order]
743
+ add_limit!(sql, options)
744
+
745
+ return sql
746
+ end
747
+
748
+ def add_limit!(sql, options)
749
+ if options[:limit] && options[:offset]
750
+ connection.add_limit_with_offset!(sql, options[:limit].to_i, options[:offset].to_i)
751
+ elsif options[:limit]
752
+ connection.add_limit_without_offset!(sql, options[:limit].to_i)
753
+ end
754
+ end
755
+
740
756
  # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
741
757
  def add_conditions!(sql, conditions)
742
758
  sql << "WHERE #{sanitize_sql(conditions)} " unless conditions.nil?
@@ -744,9 +760,11 @@ module ActiveRecord #:nodoc:
744
760
  end
745
761
 
746
762
  def type_condition
747
- " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
748
- condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}' "
749
- end + ") "
763
+ type_condition = subclasses.inject("#{table_name}.#{inheritance_column} = '#{name.demodulize}' ") do |condition, subclass|
764
+ condition << "OR #{table_name}.#{inheritance_column} = '#{subclass.name.demodulize}' "
765
+ end
766
+
767
+ return " (#{type_condition}) "
750
768
  end
751
769
 
752
770
  # Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -760,18 +778,24 @@ module ActiveRecord #:nodoc:
760
778
  # find_first(["user_name = ?", user_name]) and find_first(["user_name = ? AND password = ?", user_name, password]) respectively. Also works
761
779
  # for find_all, but using find_all_by_amount(50) that are turned into find_all(["amount = ?", 50]).
762
780
  #
763
- # It's even possible to use all the additional parameters to find_first and find_all. For example, the full interface for find_all_by_amount
764
- # is actually find_all_by_amount(amount, orderings = nil, limit = nil, joins = nil).
781
+ # It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
782
+ # is actually find_all_by_amount(amount, options).
765
783
  def method_missing(method_id, *arguments)
766
784
  method_name = method_id.id2name
767
785
 
768
786
  if method_name =~ /find_(all_by|by)_([_a-z][_a-z\d]*)/
769
- finder, attributes = ($1 == "all_by" ? :find_all : :find_first), $2.split("_and_")
787
+ finder, attributes = ($1 == "all_by" ? :all : :first), $2.split("_and_")
770
788
  attributes.each { |attr_name| super unless column_methods_hash[attr_name.intern] }
771
789
 
772
790
  attr_index = -1
773
791
  conditions = attributes.collect { |attr_name| attr_index += 1; "#{attr_name} #{arguments[attr_index].nil? ? "IS" : "="} ? " }.join(" AND ")
774
- send(finder, [conditions, *arguments[0...attributes.length]], *arguments[attributes.length..-1])
792
+
793
+ if arguments[attributes.length].is_a?(Hash)
794
+ find(finder, { :conditions => [conditions, *arguments[0...attributes.length]]}.merge(arguments[attributes.length]))
795
+ else
796
+ # deprecated API
797
+ send("find_#{finder}", [conditions, *arguments[0...attributes.length]], *arguments[attributes.length..-1])
798
+ end
775
799
  else
776
800
  super
777
801
  end
@@ -1085,7 +1109,7 @@ module ActiveRecord #:nodoc:
1085
1109
  # Delegates to id in order to allow two records of the same type and id to work with something like:
1086
1110
  # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
1087
1111
  def hash
1088
- id
1112
+ id.hash
1089
1113
  end
1090
1114
 
1091
1115
  # For checking respond_to? without searching the attributes (which is faster).