activerecord 1.0.0 → 1.1.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 (47) hide show
  1. data/CHANGELOG +102 -1
  2. data/dev-utils/eval_debugger.rb +12 -7
  3. data/lib/active_record.rb +2 -0
  4. data/lib/active_record/aggregations.rb +1 -1
  5. data/lib/active_record/associations.rb +74 -53
  6. data/lib/active_record/associations.rb.orig +555 -0
  7. data/lib/active_record/associations/association_collection.rb +74 -15
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +86 -25
  9. data/lib/active_record/associations/has_many_association.rb +48 -50
  10. data/lib/active_record/base.rb +56 -24
  11. data/lib/active_record/connection_adapters/abstract_adapter.rb +46 -3
  12. data/lib/active_record/connection_adapters/mysql_adapter.rb +15 -15
  13. data/lib/active_record/connection_adapters/postgresql_adapter.rb +128 -135
  14. data/lib/active_record/connection_adapters/sqlite_adapter.rb +76 -78
  15. data/lib/active_record/deprecated_associations.rb +1 -1
  16. data/lib/active_record/fixtures.rb +137 -54
  17. data/lib/active_record/observer.rb +1 -1
  18. data/lib/active_record/support/inflector.rb +8 -0
  19. data/lib/active_record/transactions.rb +31 -14
  20. data/rakefile +13 -5
  21. data/test/abstract_unit.rb +7 -1
  22. data/test/associations_test.rb +99 -27
  23. data/test/base_test.rb +15 -1
  24. data/test/connections/native_sqlite/connection.rb +24 -14
  25. data/test/deprecated_associations_test.rb +3 -4
  26. data/test/deprecated_associations_test.rb.orig +334 -0
  27. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  28. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  29. data/test/fixtures/bad_fixtures/blank_line +3 -0
  30. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  31. data/test/fixtures/bad_fixtures/missing_value +1 -0
  32. data/test/fixtures/company_in_module.rb +15 -1
  33. data/test/fixtures/db_definitions/mysql.sql +2 -1
  34. data/test/fixtures/db_definitions/postgresql.sql +2 -1
  35. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  36. data/test/fixtures/developers_projects/david_action_controller +2 -1
  37. data/test/fixtures/developers_projects/david_active_record +2 -1
  38. data/test/fixtures/fixture_database.sqlite +0 -0
  39. data/test/fixtures/fixture_database_2.sqlite +0 -0
  40. data/test/fixtures/project.rb +2 -1
  41. data/test/fixtures/projects/action_controller +1 -1
  42. data/test/fixtures/topics/second +1 -1
  43. data/test/fixtures_test.rb +63 -4
  44. data/test/inflector_test.rb +17 -0
  45. data/test/modules_test.rb +8 -0
  46. data/test/transactions_test.rb +16 -4
  47. metadata +10 -2
@@ -3,7 +3,7 @@ module ActiveRecord
3
3
  class AssociationCollection #:nodoc:
4
4
  alias_method :proxy_respond_to?, :respond_to?
5
5
  instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
6
-
6
+
7
7
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
8
8
  @owner = owner
9
9
  @options = options
@@ -13,45 +13,99 @@ module ActiveRecord
13
13
  end
14
14
 
15
15
  def method_missing(symbol, *args, &block)
16
- load_collection_to_array
17
- @collection_array.send(symbol, *args, &block)
16
+ load_collection
17
+ @collection.send(symbol, *args, &block)
18
18
  end
19
19
 
20
20
  def to_ary
21
- load_collection_to_array
22
- @collection_array.to_ary
21
+ load_collection
22
+ @collection.to_ary
23
23
  end
24
-
24
+
25
25
  def respond_to?(symbol)
26
26
  proxy_respond_to?(symbol) || [].respond_to?(symbol)
27
27
  end
28
+
29
+ def loaded?
30
+ !@collection.nil?
31
+ end
28
32
 
29
33
  def reload
30
- @collection_array = nil
34
+ @collection = nil
31
35
  end
32
-
33
- def concat(*records)
34
- records.flatten!
35
- records.each {|record| self << record; }
36
+
37
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
38
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
39
+ def <<(*records)
40
+ flatten_deeper(records).each do |record|
41
+ raise_on_type_mismatch(record)
42
+ insert_record(record)
43
+ @collection << record if loaded?
44
+ end
45
+ self
46
+ end
47
+
48
+ alias_method :push, :<<
49
+ alias_method :concat, :<<
50
+
51
+ # Remove +records+ from this association. Does not destroy +records+.
52
+ def delete(*records)
53
+ records = flatten_deeper(records)
54
+ records.each { |record| raise_on_type_mismatch(record) }
55
+ delete_records(records)
56
+ records.each { |record| @collection.delete(record) } if loaded?
36
57
  end
37
58
 
38
59
  def destroy_all
39
- load_collection_to_array
40
- @collection_array.each { |object| object.destroy }
41
- @collection_array = []
60
+ each { |record| record.destroy }
61
+ @collection = []
42
62
  end
43
63
 
44
64
  def size
45
- (@collection_array.nil?) ? count_records : @collection_array.size
65
+ if loaded? then @collection.size else count_records end
46
66
  end
47
67
 
48
68
  def empty?
49
69
  size == 0
50
70
  end
51
71
 
72
+ def uniq(collection = self)
73
+ collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
74
+ end
75
+
52
76
  alias_method :length, :size
77
+
78
+ protected
79
+ def loaded?
80
+ not @collection.nil?
81
+ end
82
+
83
+ def quoted_record_ids(records)
84
+ records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',')
85
+ end
86
+
87
+ def interpolate_sql_options!(options, *keys)
88
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
89
+ end
90
+
91
+ def interpolate_sql(sql, record = nil)
92
+ @owner.send(:interpolate_sql, sql, record)
93
+ end
53
94
 
54
95
  private
96
+ def load_collection
97
+ begin
98
+ @collection = find_all_records unless loaded?
99
+ rescue ActiveRecord::RecordNotFound
100
+ @collection = []
101
+ end
102
+ end
103
+
104
+ def raise_on_type_mismatch(record)
105
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
106
+ end
107
+
108
+
55
109
  def load_collection_to_array
56
110
  return unless @collection_array.nil?
57
111
  begin
@@ -65,6 +119,11 @@ module ActiveRecord
65
119
  records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
66
120
  records.dup
67
121
  end
122
+
123
+ # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
124
+ def flatten_deeper(array)
125
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
126
+ end
68
127
  end
69
128
  end
70
129
  end
@@ -1,45 +1,106 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
- class HasAndBelongsToManyCollection < AssociationCollection #:nodoc:
3
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
4
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
5
5
  super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
-
7
- @association_foreign_key = options[:association_foreign_key] || association_class_name.downcase + "_id"
6
+
7
+ @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id"
8
8
  association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
9
9
  @join_table = join_table
10
10
  @order = options[:order] || "t.#{@owner.class.primary_key}"
11
11
 
12
+ interpolate_sql_options!(options, :finder_sql, :delete_sql)
12
13
  @finder_sql = options[:finder_sql] ||
13
- "SELECT t.* FROM #{association_table_name} t, #{@join_table} j " +
14
+ "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
14
15
  "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
15
- "j.#{association_class_primary_key_name} = '#{@owner.id}' ORDER BY #{@order}"
16
+ "j.#{association_class_primary_key_name} = '#{@owner.id}' " +
17
+ (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
18
+ "ORDER BY #{@order}"
16
19
  end
17
-
18
- def <<(record)
19
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
20
- sql = @options[:insert_sql] ||
21
- "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) " +
22
- "VALUES ('#{@owner.id}', '#{record.id}')"
23
- @owner.connection.execute(sql)
24
- @collection_array << record unless @collection_array.nil?
20
+
21
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
22
+ def clear
23
+ return self if size == 0 # forces load_collection if hasn't happened already
24
+
25
+ if sql = @options[:delete_sql]
26
+ each { |record| @owner.connection.execute(sql) }
27
+ elsif @options[:conditions]
28
+ sql =
29
+ "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " +
30
+ "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
31
+ @owner.connection.execute(sql)
32
+ else
33
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
34
+ @owner.connection.execute(sql)
35
+ end
36
+
37
+ @collection = []
38
+ self
25
39
  end
26
-
27
- def delete(records)
28
- records = duplicated_records_array(records)
29
- sql = @options[:delete_sql] || "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
30
- ids = records.map { |record| "'" + record.id.to_s + "'" }.join(',')
31
- @owner.connection.delete "#{sql} AND #{@association_foreign_key} in (#{ids})"
32
- records.each {|record| @collection_array.delete(record) } unless @collection_array.nil?
40
+
41
+ def find(association_id = nil, &block)
42
+ if block_given? || @options[:finder_sql]
43
+ load_collection
44
+ @collection.find(&block)
45
+ else
46
+ if loaded?
47
+ find_all { |record| record.id == association_id.to_i }.first
48
+ else
49
+ find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first
50
+ end
51
+ end
33
52
  end
34
-
53
+
54
+ def push_with_attributes(record, join_attributes = {})
55
+ raise_on_type_mismatch(record)
56
+ insert_record_with_join_attributes(record, join_attributes)
57
+ join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
58
+ @collection << record if loaded?
59
+ self
60
+ end
61
+
62
+ alias :concat_with_attributes :push_with_attributes
63
+
64
+ def size
65
+ @options[:uniq] ? count_records : super
66
+ end
67
+
35
68
  protected
36
- def find_all_records
37
- @association_class.find_by_sql(@finder_sql)
69
+ def find_all_records(sql = @finder_sql)
70
+ records = @association_class.find_by_sql(sql)
71
+ @options[:uniq] ? uniq(records) : records
38
72
  end
39
73
 
40
74
  def count_records
41
- load_collection_to_array
42
- @collection_array.size
75
+ load_collection
76
+ @collection.size
77
+ end
78
+
79
+ def insert_record(record)
80
+ if @options[:insert_sql]
81
+ @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
82
+ else
83
+ sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')"
84
+ @owner.connection.execute(sql)
85
+ end
86
+ end
87
+
88
+ def insert_record_with_join_attributes(record, join_attributes)
89
+ attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
90
+ sql =
91
+ "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
92
+ "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
93
+ @owner.connection.execute(sql)
94
+ end
95
+
96
+ def delete_records(records)
97
+ if sql = @options[:delete_sql]
98
+ records.each { |record| @owner.connection.execute(sql) }
99
+ else
100
+ ids = quoted_record_ids(records)
101
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})"
102
+ @owner.connection.execute(sql)
103
+ end
43
104
  end
44
105
  end
45
106
  end
@@ -3,76 +3,65 @@ module ActiveRecord
3
3
  class HasManyAssociation < AssociationCollection #:nodoc:
4
4
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
5
  super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
- @conditions = options[:conditions]
7
-
6
+ @conditions = @association_class.send(:sanitize_conditions, options[:conditions])
7
+
8
8
  if options[:finder_sql]
9
- @counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
10
- @finder_sql = options[:finder_sql]
9
+ @finder_sql = interpolate_sql(options[:finder_sql])
10
+ @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
11
11
  else
12
- @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + @conditions : ""}"
13
- @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
14
- end
15
- end
16
-
17
- def <<(record)
18
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
19
- record.send(@association_class_primary_key_name + "=", @owner.id)
20
- record.save(false)
21
- @collection_array << record unless @collection_array.nil?
22
- end
23
-
24
- def delete(records)
25
- duplicated_records_array(records).each do |record|
26
- next if record.send(@association_class_primary_key_name) != @owner.id
27
- record.send(@association_class_primary_key_name + "=", nil)
28
- record.save(false)
29
- @collection_array.delete(record) unless @collection_array.nil?
12
+ @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
13
+ @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
30
14
  end
31
15
  end
32
-
16
+
33
17
  def create(attributes = {})
34
- # We can't use the regular Base.create method as the foreign key might be a protected attribute, hence the repetion
35
- record = @association_class.new(attributes || {})
36
- record.send(@association_class_primary_key_name + "=", @owner.id)
18
+ # Can't use Base.create since the foreign key may be a protected attribute.
19
+ record = build(attributes)
37
20
  record.save
38
-
39
- @collection_array << record unless @collection_array.nil?
40
-
41
- return record
21
+ @collection << record if loaded?
22
+ record
42
23
  end
43
24
 
44
25
  def build(attributes = {})
45
- association = @association_class.new
46
- association.attributes = attributes.merge({ "#{@association_class_primary_key_name}" => @owner.id})
47
- association
26
+ record = @association_class.new(attributes)
27
+ record[@association_class_primary_key_name] = @owner.id
28
+ record
48
29
  end
49
-
30
+
50
31
  def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
51
32
  if block_given? || @options[:finder_sql]
52
- load_collection_to_array
53
- @collection_array.send(:find_all, &block)
33
+ load_collection
34
+ @collection.find_all(&block)
54
35
  else
55
36
  @association_class.find_all(
56
- "#{@association_class_primary_key_name} = '#{@owner.id}' " +
57
- "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + runtime_conditions : ""}",
58
- orderings,
59
- limit,
60
- joins
61
- )
37
+ "#{@association_class_primary_key_name} = '#{@owner.id}' " +
38
+ "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
39
+ orderings,
40
+ limit,
41
+ joins
42
+ )
62
43
  end
63
44
  end
64
45
 
65
46
  def find(association_id = nil, &block)
66
47
  if block_given? || @options[:finder_sql]
67
- load_collection_to_array
68
- return @collection_array.send(:find, &block)
48
+ load_collection
49
+ @collection.find(&block)
69
50
  else
70
- @association_class.find_on_conditions(
71
- association_id, "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
72
- )
51
+ @association_class.find_on_conditions(association_id,
52
+ "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
53
+ )
73
54
  end
74
55
  end
75
-
56
+
57
+ # Removes all records from this association. Returns +self+ so
58
+ # method calls may be chained.
59
+ def clear
60
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'")
61
+ @collection = []
62
+ self
63
+ end
64
+
76
65
  protected
77
66
  def find_all_records
78
67
  if @options[:finder_sql]
@@ -97,8 +86,17 @@ module ActiveRecord
97
86
  end
98
87
 
99
88
  def cached_counter_attribute_name
100
- @association_name + "_count"
89
+ "#{@association_name}_count"
90
+ end
91
+
92
+ def insert_record(record)
93
+ record.update_attribute(@association_class_primary_key_name, @owner.id)
94
+ end
95
+
96
+ def delete_records(records)
97
+ ids = quoted_record_ids(records)
98
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})")
101
99
  end
102
100
  end
103
101
  end
104
- end
102
+ end
@@ -111,7 +111,7 @@ module ActiveRecord #:nodoc:
111
111
  # end
112
112
  #
113
113
  # user = User.create("preferences" => %w( one two three ))
114
- # User.find(user.id).preferences # => raises SerializationTypeMismatch
114
+ # User.find(user.id).preferences # raises SerializationTypeMismatch
115
115
  #
116
116
  # == Single table inheritance
117
117
  #
@@ -220,7 +220,7 @@ module ActiveRecord #:nodoc:
220
220
  # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
221
221
  # +RecordNotFound+ is raised if no record can be found.
222
222
  def find(*ids)
223
- ids = [ ids ].flatten.compact
223
+ ids = ids.flatten.compact.uniq
224
224
 
225
225
  if ids.length > 1
226
226
  ids_list = ids.map{ |id| "'#{sanitize(id)}'" }.join(", ")
@@ -234,7 +234,7 @@ module ActiveRecord #:nodoc:
234
234
  elsif ids.length == 1
235
235
  id = ids.first
236
236
  sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{sanitize(id)}'"
237
- sql << " AND #{type_condition}" unless descents_from_active_record?
237
+ sql << " AND #{type_condition}" unless descends_from_active_record?
238
238
 
239
239
  if record = connection.select_one(sql, "#{name} Find")
240
240
  instantiate(record)
@@ -275,7 +275,7 @@ module ActiveRecord #:nodoc:
275
275
  def find_by_sql(sql)
276
276
  connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
277
277
  end
278
-
278
+
279
279
  # Returns the object for the first record responding to the conditions in +conditions+,
280
280
  # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
281
281
  # be used to create the object. In such cases, it might be beneficial to also specify
@@ -461,7 +461,7 @@ module ActiveRecord #:nodoc:
461
461
 
462
462
  # Defines the column name for use with single table inheritance -- can be overridden in subclasses.
463
463
  def inheritance_column
464
- "type"
464
+ "type"
465
465
  end
466
466
 
467
467
  # Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
@@ -485,7 +485,19 @@ module ActiveRecord #:nodoc:
485
485
  # Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
486
486
  # and columns used for single table inheritance has been removed.
487
487
  def content_columns
488
- columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
488
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
489
+ end
490
+
491
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
492
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
493
+ # is available.
494
+ def column_methods_hash
495
+ @dynamic_methods_hash ||= columns_hash.keys.inject(Hash.new(false)) do |methods, attr|
496
+ methods[attr.to_sym] = true
497
+ methods["#{attr}=".to_sym] = true
498
+ methods["#{attr}?".to_sym] = true
499
+ methods
500
+ end
489
501
  end
490
502
 
491
503
  # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
@@ -494,7 +506,7 @@ module ActiveRecord #:nodoc:
494
506
  attribute_key_name.gsub(/_/, " ").capitalize unless attribute_key_name.nil?
495
507
  end
496
508
 
497
- def descents_from_active_record? # :nodoc:
509
+ def descends_from_active_record? # :nodoc:
498
510
  superclass == Base
499
511
  end
500
512
 
@@ -513,10 +525,12 @@ module ActiveRecord #:nodoc:
513
525
  # project.milestones << Milestone.find_all
514
526
  # end
515
527
  def benchmark(title)
528
+ result = nil
516
529
  logger.level = Logger::ERROR
517
- bm = Benchmark.measure { yield }
530
+ bm = Benchmark.measure { result = yield }
518
531
  logger.level = Logger::DEBUG
519
532
  logger.info "#{title} (#{sprintf("%f", bm.real)})"
533
+ return result
520
534
  end
521
535
 
522
536
  private
@@ -531,7 +545,7 @@ module ActiveRecord #:nodoc:
531
545
  # Returns true if the +record+ has a single table inheritance column and is using it.
532
546
  def record_with_type?(record)
533
547
  record.include?(inheritance_column) && !record[inheritance_column].nil? &&
534
- !record[inheritance_column].empty?
548
+ !record[inheritance_column].empty?
535
549
  end
536
550
 
537
551
  # Returns the name of the type of the record using the current module as a prefix. So descendents of
@@ -543,12 +557,12 @@ module ActiveRecord #:nodoc:
543
557
  # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
544
558
  def add_conditions!(sql, conditions)
545
559
  sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
546
- sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descents_from_active_record?
560
+ sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
547
561
  end
548
562
 
549
563
  def type_condition
550
564
  " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
551
- condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}' "
565
+ condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}'"
552
566
  end + ") "
553
567
  end
554
568
 
@@ -614,7 +628,7 @@ module ActiveRecord #:nodoc:
614
628
  # Every Active Record class must use "id" as their primary ID. This getter overwrites the native
615
629
  # id method, which isn't being used in this context.
616
630
  def id
617
- read_attribute(self.class.primary_key)
631
+ read_attribute(self.class.primary_key) unless new_record?
618
632
  end
619
633
 
620
634
  # Sets the primary ID.
@@ -716,20 +730,30 @@ module ActiveRecord #:nodoc:
716
730
  def column_for_attribute(name)
717
731
  self.class.columns_hash[name]
718
732
  end
719
-
733
+
720
734
  # Returns true if the +comparison_object+ is of the same type and has the same id.
721
735
  def ==(comparison_object)
722
736
  comparison_object.instance_of?(self.class) && comparison_object.id == id
723
737
  end
724
738
 
739
+ # Delegates to ==
740
+ def eql?(comparison_object)
741
+ self == (comparison_object)
742
+ end
743
+
744
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
745
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
746
+ def hash
747
+ id
748
+ end
749
+
725
750
  # For checking respond_to? without searching the attributes (which is faster).
726
751
  alias_method :respond_to_without_attributes?, :respond_to?
727
752
 
728
753
  # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
729
754
  # person.respond_to?("name?") which will all return true.
730
755
  def respond_to?(method)
731
- @@dynamic_methods ||= attribute_names + attribute_names.collect { |attr| attr + "=" } + attribute_names.collect { |attr| attr + "?" }
732
- @@dynamic_methods.include?(method.to_s) ? true : respond_to_without_attributes?(method)
756
+ self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
733
757
  end
734
758
 
735
759
  private
@@ -754,7 +778,7 @@ module ActiveRecord #:nodoc:
754
778
  "(#{quoted_column_names.join(', ')}) " +
755
779
  "VALUES(#{attributes_with_quotes.values.join(', ')})",
756
780
  "#{self.class.name} Create",
757
- self.class.primary_key, self.id
781
+ self.class.primary_key, self.id
758
782
  )
759
783
 
760
784
  @new_record = false
@@ -765,9 +789,9 @@ module ActiveRecord #:nodoc:
765
789
  # set Reply[Reply.inheritance_column] = "Reply" yourself. No such attribute would be set for objects of the
766
790
  # Message class in that example.
767
791
  def ensure_proper_type
768
- unless self.class.descents_from_active_record?
769
- write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name))
770
- end
792
+ unless self.class.descends_from_active_record?
793
+ write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name))
794
+ end
771
795
  end
772
796
 
773
797
  # Allows access to the object attributes, which are held in the @attributes hash, as were
@@ -781,11 +805,13 @@ module ActiveRecord #:nodoc:
781
805
  def method_missing(method_id, *arguments)
782
806
  method_name = method_id.id2name
783
807
 
808
+
809
+
784
810
  if method_name =~ read_method? && @attributes.include?($1)
785
811
  return read_attribute($1)
786
- elsif method_name =~ write_method?
812
+ elsif method_name =~ write_method? && @attributes.include?($1)
787
813
  write_attribute($1, arguments[0])
788
- elsif method_name =~ query_method?
814
+ elsif method_name =~ query_method? && @attributes.include?($1)
789
815
  return query_attribute($1)
790
816
  else
791
817
  super
@@ -884,6 +910,12 @@ module ActiveRecord #:nodoc:
884
910
  connection.quote(value, column)
885
911
  end
886
912
 
913
+ # Interpolate custom sql string in instance context.
914
+ # Optional record argument is meant for custom insert_sql.
915
+ def interpolate_sql(sql, record = nil)
916
+ instance_eval("%(#{sql})")
917
+ end
918
+
887
919
  # Initializes the attributes array with keys matching the columns from the linked table and
888
920
  # the values matching the corresponding default value of that column, so
889
921
  # that a new instance, or one populated from a passed-in Hash, still has all the attributes
@@ -949,8 +981,8 @@ module ActiveRecord #:nodoc:
949
981
  hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
950
982
  end
951
983
 
952
- def quoted_column_names
953
- attributes_with_quotes.keys.collect { |column_name| connection.quote_column_name(column_name) }
984
+ def quoted_column_names(attributes = attributes_with_quotes)
985
+ attributes.keys.collect { |column_name| connection.quote_column_name(column_name) }
954
986
  end
955
987
 
956
988
  def quote_columns(column_quoter, hash)
@@ -982,4 +1014,4 @@ module ActiveRecord #:nodoc:
982
1014
  string[0..3] == "--- "
983
1015
  end
984
1016
  end
985
- end
1017
+ end