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.
- data/CHANGELOG +98 -0
- data/install.rb +1 -0
- data/lib/active_record.rb +1 -0
- data/lib/active_record/acts/list.rb +19 -16
- data/lib/active_record/associations.rb +164 -164
- data/lib/active_record/associations/association_collection.rb +44 -71
- data/lib/active_record/associations/association_proxy.rb +76 -0
- data/lib/active_record/associations/belongs_to_association.rb +74 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +34 -21
- data/lib/active_record/associations/has_many_association.rb +34 -30
- data/lib/active_record/associations/has_one_association.rb +48 -0
- data/lib/active_record/base.rb +62 -18
- data/lib/active_record/callbacks.rb +17 -8
- data/lib/active_record/connection_adapters/abstract_adapter.rb +11 -10
- data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -1
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +94 -73
- data/lib/active_record/deprecated_associations.rb +46 -8
- data/lib/active_record/fixtures.rb +1 -1
- data/lib/active_record/observer.rb +5 -1
- data/lib/active_record/support/binding_of_caller.rb +72 -68
- data/lib/active_record/support/breakpoint.rb +526 -524
- data/lib/active_record/support/class_inheritable_attributes.rb +105 -29
- data/lib/active_record/support/core_ext.rb +1 -0
- data/lib/active_record/support/core_ext/hash.rb +5 -0
- data/lib/active_record/support/core_ext/hash/keys.rb +35 -0
- data/lib/active_record/support/core_ext/numeric.rb +7 -0
- data/lib/active_record/support/core_ext/numeric/bytes.rb +33 -0
- data/lib/active_record/support/core_ext/numeric/time.rb +59 -0
- data/lib/active_record/support/core_ext/string.rb +5 -0
- data/lib/active_record/support/core_ext/string/inflections.rb +41 -0
- data/lib/active_record/support/dependencies.rb +1 -14
- data/lib/active_record/support/inflector.rb +6 -6
- data/lib/active_record/support/misc.rb +0 -24
- data/lib/active_record/validations.rb +34 -1
- data/lib/active_record/vendor/mysql411.rb +305 -0
- data/rakefile +11 -2
- data/test/abstract_unit.rb +1 -2
- data/test/associations_test.rb +234 -23
- data/test/base_test.rb +50 -1
- data/test/callbacks_test.rb +16 -0
- data/test/connections/native_mysql/connection.rb +2 -2
- data/test/connections/native_sqlite3/connection.rb +34 -0
- data/test/deprecated_associations_test.rb +36 -2
- data/test/fixtures/company.rb +2 -0
- data/test/fixtures/computer.rb +3 -0
- data/test/fixtures/computers.yml +3 -0
- data/test/fixtures/db_definitions/db2.sql +5 -0
- data/test/fixtures/db_definitions/mysql.sql +5 -0
- data/test/fixtures/db_definitions/postgresql.sql +5 -0
- data/test/fixtures/db_definitions/sqlite.sql +5 -0
- data/test/fixtures/db_definitions/sqlserver.sql +5 -1
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/validations_test.rb +21 -0
- 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
|
-
|
22
|
-
@
|
5
|
+
load_target
|
6
|
+
@target.to_ary
|
23
7
|
end
|
24
8
|
|
25
|
-
def
|
26
|
-
|
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
|
-
|
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
|
-
@
|
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| @
|
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
|
-
@
|
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 @
|
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
|
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
|
-
|
93
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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,
|
5
|
-
super
|
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
|
-
|
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
|
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
|
-
@
|
38
|
+
@target = []
|
38
39
|
self
|
39
40
|
end
|
40
41
|
|
41
42
|
def find_first
|
42
|
-
|
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 =
|
60
|
+
record = load_target.detect { |record| id == record.id }
|
60
61
|
expects_array? ? [record] : record
|
61
62
|
else
|
62
|
-
|
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 =
|
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
|
-
@
|
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
|
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
|
-
|
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
|
-
|
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
|