activerecord 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (55) hide show
  1. data/CHANGELOG +98 -0
  2. data/install.rb +1 -0
  3. data/lib/active_record.rb +1 -0
  4. data/lib/active_record/acts/list.rb +19 -16
  5. data/lib/active_record/associations.rb +164 -164
  6. data/lib/active_record/associations/association_collection.rb +44 -71
  7. data/lib/active_record/associations/association_proxy.rb +76 -0
  8. data/lib/active_record/associations/belongs_to_association.rb +74 -0
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +34 -21
  10. data/lib/active_record/associations/has_many_association.rb +34 -30
  11. data/lib/active_record/associations/has_one_association.rb +48 -0
  12. data/lib/active_record/base.rb +62 -18
  13. data/lib/active_record/callbacks.rb +17 -8
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +11 -10
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -1
  17. data/lib/active_record/connection_adapters/sqlite_adapter.rb +94 -73
  18. data/lib/active_record/deprecated_associations.rb +46 -8
  19. data/lib/active_record/fixtures.rb +1 -1
  20. data/lib/active_record/observer.rb +5 -1
  21. data/lib/active_record/support/binding_of_caller.rb +72 -68
  22. data/lib/active_record/support/breakpoint.rb +526 -524
  23. data/lib/active_record/support/class_inheritable_attributes.rb +105 -29
  24. data/lib/active_record/support/core_ext.rb +1 -0
  25. data/lib/active_record/support/core_ext/hash.rb +5 -0
  26. data/lib/active_record/support/core_ext/hash/keys.rb +35 -0
  27. data/lib/active_record/support/core_ext/numeric.rb +7 -0
  28. data/lib/active_record/support/core_ext/numeric/bytes.rb +33 -0
  29. data/lib/active_record/support/core_ext/numeric/time.rb +59 -0
  30. data/lib/active_record/support/core_ext/string.rb +5 -0
  31. data/lib/active_record/support/core_ext/string/inflections.rb +41 -0
  32. data/lib/active_record/support/dependencies.rb +1 -14
  33. data/lib/active_record/support/inflector.rb +6 -6
  34. data/lib/active_record/support/misc.rb +0 -24
  35. data/lib/active_record/validations.rb +34 -1
  36. data/lib/active_record/vendor/mysql411.rb +305 -0
  37. data/rakefile +11 -2
  38. data/test/abstract_unit.rb +1 -2
  39. data/test/associations_test.rb +234 -23
  40. data/test/base_test.rb +50 -1
  41. data/test/callbacks_test.rb +16 -0
  42. data/test/connections/native_mysql/connection.rb +2 -2
  43. data/test/connections/native_sqlite3/connection.rb +34 -0
  44. data/test/deprecated_associations_test.rb +36 -2
  45. data/test/fixtures/company.rb +2 -0
  46. data/test/fixtures/computer.rb +3 -0
  47. data/test/fixtures/computers.yml +3 -0
  48. data/test/fixtures/db_definitions/db2.sql +5 -0
  49. data/test/fixtures/db_definitions/mysql.sql +5 -0
  50. data/test/fixtures/db_definitions/postgresql.sql +5 -0
  51. data/test/fixtures/db_definitions/sqlite.sql +5 -0
  52. data/test/fixtures/db_definitions/sqlserver.sql +5 -1
  53. data/test/fixtures/fixture_database.sqlite +0 -0
  54. data/test/validations_test.rb +21 -0
  55. metadata +22 -2
@@ -1,51 +1,34 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
- class AssociationCollection #:nodoc:
4
- alias_method :proxy_respond_to?, :respond_to?
5
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
6
-
7
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
8
- @owner = owner
9
- @options = options
10
- @association_name = association_name
11
- @association_class = eval(association_class_name)
12
- @association_class_primary_key_name = association_class_primary_key_name
13
- end
14
-
15
- def method_missing(symbol, *args, &block)
16
- load_collection
17
- @collection.send(symbol, *args, &block)
18
- end
19
-
3
+ class AssociationCollection < AssociationProxy #:nodoc:
20
4
  def to_ary
21
- load_collection
22
- @collection.to_ary
5
+ load_target
6
+ @target.to_ary
23
7
  end
24
8
 
25
- def respond_to?(symbol, include_priv = false)
26
- proxy_respond_to?(symbol, include_priv) || [].respond_to?(symbol, include_priv)
9
+ def reset
10
+ @target = []
11
+ @loaded = false
27
12
  end
28
13
 
29
- def loaded?
30
- !@collection.nil?
31
- end
32
-
33
14
  def reload
34
- @collection = nil
15
+ reset
35
16
  end
36
17
 
37
18
  # Add +records+ to this association. Returns +self+ so method calls may be chained.
38
19
  # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
39
20
  def <<(*records)
21
+ result = true
22
+ load_target
40
23
  @owner.transaction do
41
24
  flatten_deeper(records).each do |record|
42
25
  raise_on_type_mismatch(record)
43
- insert_record(record)
44
- @collection << record if loaded?
26
+ result &&= insert_record(record) unless @owner.new_record?
27
+ @target << record
45
28
  end
46
29
  end
47
30
 
48
- self
31
+ result and self
49
32
  end
50
33
 
51
34
  alias_method :push, :<<
@@ -54,11 +37,13 @@ module ActiveRecord
54
37
  # Remove +records+ from this association. Does not destroy +records+.
55
38
  def delete(*records)
56
39
  records = flatten_deeper(records)
40
+ records.each { |record| raise_on_type_mismatch(record) }
41
+ records.reject! { |record| @target.delete(record) if record.new_record? }
42
+ return if records.empty?
57
43
 
58
44
  @owner.transaction do
59
- records.each { |record| raise_on_type_mismatch(record) }
60
45
  delete_records(records)
61
- records.each { |record| @collection.delete(record) } if loaded?
46
+ records.each { |record| @target.delete(record) }
62
47
  end
63
48
  end
64
49
 
@@ -67,65 +52,53 @@ module ActiveRecord
67
52
  each { |record| record.destroy }
68
53
  end
69
54
 
70
- @collection = []
55
+ @target = []
71
56
  end
72
57
 
58
+ def create(attributes = {})
59
+ # Can't use Base.create since the foreign key may be a protected attribute.
60
+ record = build(attributes)
61
+ record.save unless @owner.new_record?
62
+ record
63
+ end
64
+
65
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
66
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
67
+ # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
73
68
  def size
74
- if loaded? then @collection.size else count_records end
69
+ if loaded? then @target.size else count_records end
70
+ end
71
+
72
+ # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
73
+ # whether the collection is empty, use collection.length.zero? instead of collection.empty?
74
+ def length
75
+ load_target.size
75
76
  end
76
77
 
77
78
  def empty?
78
- size == 0
79
+ size.zero?
79
80
  end
80
81
 
81
82
  def uniq(collection = self)
82
83
  collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
83
84
  end
84
-
85
- alias_method :length, :size
86
-
87
- protected
88
- def loaded?
89
- not @collection.nil?
90
- end
91
85
 
92
- def quoted_record_ids(records)
93
- records.map { |record| record.quoted_id }.join(',')
94
- end
95
-
96
- def interpolate_sql_options!(options, *keys)
97
- keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
98
- end
99
-
100
- def interpolate_sql(sql, record = nil)
101
- @owner.send(:interpolate_sql, sql, record)
102
- end
103
-
104
- def sanitize_sql(sql)
105
- @association_class.send(:sanitize_sql, sql)
106
- end
86
+ def replace(other_array)
87
+ other_array.each{ |val| raise_on_type_mismatch(val) }
107
88
 
108
- def extract_options_from_args!(args)
109
- @owner.send(:extract_options_from_args!, args)
110
- end
89
+ @target = other_array
90
+ @loaded = true
91
+ end
111
92
 
112
93
  private
113
- def load_collection
114
- if loaded?
115
- @collection
116
- else
117
- begin
118
- @collection = find_all_records
119
- rescue ActiveRecord::RecordNotFound
120
- @collection = []
121
- end
122
- end
123
- end
124
-
125
94
  def raise_on_type_mismatch(record)
126
95
  raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
127
96
  end
128
97
 
98
+ def target_obsolete?
99
+ false
100
+ end
101
+
129
102
  # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
130
103
  def flatten_deeper(array)
131
104
  array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
@@ -0,0 +1,76 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class AssociationProxy #:nodoc:
4
+ alias_method :proxy_respond_to?, :respond_to?
5
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^send)/ }
6
+
7
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
8
+ @owner = owner
9
+ @options = options
10
+ @association_name = association_name
11
+ @association_class = eval(association_class_name)
12
+ @association_class_primary_key_name = association_class_primary_key_name
13
+
14
+ reset
15
+ end
16
+
17
+ def method_missing(symbol, *args, &block)
18
+ load_target
19
+ @target.send(symbol, *args, &block)
20
+ end
21
+
22
+ def respond_to?(symbol, include_priv = false)
23
+ load_target
24
+ proxy_respond_to?(symbol, include_priv) || @target.respond_to?(symbol, include_priv)
25
+ end
26
+
27
+ def loaded?
28
+ @loaded
29
+ end
30
+
31
+ protected
32
+ def quoted_record_ids(records)
33
+ records.map { |record| record.quoted_id }.join(',')
34
+ end
35
+
36
+ def interpolate_sql_options!(options, *keys)
37
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
38
+ end
39
+
40
+ def interpolate_sql(sql, record = nil)
41
+ @owner.send(:interpolate_sql, sql, record)
42
+ end
43
+
44
+ def sanitize_sql(sql)
45
+ @association_class.send(:sanitize_sql, sql)
46
+ end
47
+
48
+ def extract_options_from_args!(args)
49
+ @owner.send(:extract_options_from_args!, args)
50
+ end
51
+
52
+ private
53
+ def load_target
54
+ if !@owner.new_record? || foreign_key_present
55
+ begin
56
+ @target = find_target if not loaded?
57
+ rescue ActiveRecord::RecordNotFound
58
+ reset
59
+ end
60
+ end
61
+ @loaded = true if @target
62
+ @target
63
+ end
64
+
65
+ # Can be overwritten by associations that might have the foreign key available for an association without
66
+ # having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
67
+ def foreign_key_present
68
+ false
69
+ end
70
+
71
+ def raise_on_type_mismatch(record)
72
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,74 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class BelongsToAssociation < AssociationProxy #:nodoc:
4
+
5
+ def reset
6
+ @target = nil
7
+ @loaded = false
8
+ end
9
+
10
+ def reload
11
+ reset
12
+ load_target
13
+ end
14
+
15
+ def create(attributes = {})
16
+ record = build(attributes)
17
+ record.save
18
+ record
19
+ end
20
+
21
+ def build(attributes = {})
22
+ record = @association_class.new(attributes)
23
+ replace(record, true)
24
+ record
25
+ end
26
+
27
+ def replace(obj, dont_save = false)
28
+ if obj.nil?
29
+ @target = @owner[@association_class_primary_key_name] = nil
30
+ else
31
+ raise_on_type_mismatch(obj) unless obj.nil?
32
+
33
+ @target = obj
34
+ @owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
35
+ end
36
+ @loaded = true
37
+ end
38
+
39
+ # Ugly workaround - .nil? is done in C and the method_missing trick doesn't work when we pretend to be nil
40
+ def nil?
41
+ load_target
42
+ @target.nil?
43
+ end
44
+
45
+ private
46
+ def find_target
47
+ if @options[:conditions]
48
+ @association_class.find_on_conditions(@owner[@association_class_primary_key_name], interpolate_sql(@options[:conditions]))
49
+ else
50
+ @association_class.find(@owner[@association_class_primary_key_name])
51
+ end
52
+ end
53
+
54
+ def foreign_key_present
55
+ !@owner[@association_class_primary_key_name].nil?
56
+ end
57
+
58
+ def target_obsolete?
59
+ @owner[@association_class_primary_key_name] != @target.id
60
+ end
61
+
62
+ def construct_sql
63
+ # no sql to construct
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ class NilClass #:nodoc:
70
+ # Ugly workaround - nil comparison is usually done in C and so a proxy object pretending to be nil doesn't work.
71
+ def ==(other)
72
+ other.nil?
73
+ end
74
+ end
@@ -1,26 +1,27 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
3
  class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
4
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
+ super
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
9
- @join_table = join_table
8
+ @association_table_name = options[:table_name] || @association_class.table_name
9
+ @join_table = options[:join_table]
10
10
  @order = options[:order] || "t.#{@association_class.primary_key}"
11
11
 
12
- interpolate_sql_options!(options, :finder_sql, :delete_sql)
13
- @finder_sql = options[:finder_sql] ||
14
- "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
15
- "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
16
- "j.#{association_class_primary_key_name} = #{@owner.quoted_id} " +
17
- (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
18
- "ORDER BY #{@order}"
12
+ construct_sql
19
13
  end
20
14
 
15
+ def build(attributes = {})
16
+ load_target
17
+ record = @association_class.new(attributes)
18
+ @target << record
19
+ record
20
+ end
21
+
21
22
  # Removes all records from this association. Returns +self+ so method calls may be chained.
22
23
  def clear
23
- return self if size == 0 # forces load_collection if hasn't happened already
24
+ return self if size == 0 # forces load_target if hasn't happened already
24
25
 
25
26
  if sql = @options[:delete_sql]
26
27
  each { |record| @owner.connection.execute(sql) }
@@ -34,12 +35,12 @@ module ActiveRecord
34
35
  @owner.connection.execute(sql)
35
36
  end
36
37
 
37
- @collection = []
38
+ @target = []
38
39
  self
39
40
  end
40
41
 
41
42
  def find_first
42
- load_collection.first
43
+ load_target.first
43
44
  end
44
45
 
45
46
  def find(*args)
@@ -56,16 +57,16 @@ module ActiveRecord
56
57
  elsif @options[:finder_sql]
57
58
  if ids.size == 1
58
59
  id = ids.first
59
- record = load_collection.detect { |record| id == record.id }
60
+ record = load_target.detect { |record| id == record.id }
60
61
  expects_array? ? [record] : record
61
62
  else
62
- load_collection.select { |record| ids.include?(record.id) }
63
+ load_target.select { |record| ids.include?(record.id) }
63
64
  end
64
65
 
65
66
  # Otherwise, construct a query.
66
67
  else
67
68
  ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
68
- records = find_all_records(@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}) ORDER BY"))
69
70
  if records.size == ids.size
70
71
  if ids.size == 1 and !expects_array
71
72
  records.first
@@ -82,7 +83,7 @@ module ActiveRecord
82
83
  raise_on_type_mismatch(record)
83
84
  insert_record_with_join_attributes(record, join_attributes)
84
85
  join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
85
- @collection << record if loaded?
86
+ @target << record
86
87
  self
87
88
  end
88
89
 
@@ -93,16 +94,17 @@ module ActiveRecord
93
94
  end
94
95
 
95
96
  protected
96
- def find_all_records(sql = @finder_sql)
97
+ def find_target(sql = @finder_sql)
97
98
  records = @association_class.find_by_sql(sql)
98
99
  @options[:uniq] ? uniq(records) : records
99
100
  end
100
101
 
101
102
  def count_records
102
- load_collection.size
103
+ load_target.size
103
104
  end
104
105
 
105
106
  def insert_record(record)
107
+ return false unless record.save
106
108
  if @options[:insert_sql]
107
109
  @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
108
110
  else
@@ -110,6 +112,7 @@ module ActiveRecord
110
112
  "VALUES (#{@owner.quoted_id},#{record.quoted_id})"
111
113
  @owner.connection.execute(sql)
112
114
  end
115
+ true
113
116
  end
114
117
 
115
118
  def insert_record_with_join_attributes(record, join_attributes)
@@ -129,6 +132,16 @@ module ActiveRecord
129
132
  @owner.connection.execute(sql)
130
133
  end
131
134
  end
132
- end
135
+
136
+ def construct_sql
137
+ interpolate_sql_options!(@options, :finder_sql, :delete_sql)
138
+ @finder_sql = @options[:finder_sql] ||
139
+ "SELECT t.*, j.* FROM #{@association_table_name} t, #{@join_table} j " +
140
+ "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
141
+ "j.#{@association_class_primary_key_name} = #{@owner.quoted_id} " +
142
+ (@options[:conditions] ? " AND " + interpolate_sql(@options[:conditions]) : "") + " " +
143
+ "ORDER BY #{@order}"
144
+ end
145
+ end
133
146
  end
134
147
  end