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
@@ -271,6 +271,39 @@ module ActiveRecord
|
|
271
271
|
end
|
272
272
|
end
|
273
273
|
end
|
274
|
+
|
275
|
+
# Validates whether the associated object or objects are all themselves valid. Works with any kind of assocation.
|
276
|
+
#
|
277
|
+
# class Book < ActiveRecord::Base
|
278
|
+
# has_many :pages
|
279
|
+
# belongs_to :library
|
280
|
+
#
|
281
|
+
# validates_associated :pages, :library
|
282
|
+
# end
|
283
|
+
#
|
284
|
+
# Warning: If, after the above definition, you then wrote:
|
285
|
+
#
|
286
|
+
# class Page < ActiveRecord::Base
|
287
|
+
# belongs_to :book
|
288
|
+
#
|
289
|
+
# validates_associated :book
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# this would specify a circular dependency and cause infinite recursion. The Rails team recommends against this practice.
|
293
|
+
#
|
294
|
+
# Configuration options:
|
295
|
+
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
|
296
|
+
def validates_associated(*attr_names)
|
297
|
+
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
|
298
|
+
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
|
299
|
+
|
300
|
+
for attr_name in attr_names
|
301
|
+
class_eval(%(#{validation_method(configuration[:on])} %{
|
302
|
+
errors.add("#{attr_name}", "#{configuration[:message]}") unless
|
303
|
+
(#{attr_name}.is_a?(Array) ? #{attr_name} : [#{attr_name}]).inject(true){ |memo, record| memo and (record.nil? or record.valid?) }
|
304
|
+
}))
|
305
|
+
end
|
306
|
+
end
|
274
307
|
|
275
308
|
|
276
309
|
private
|
@@ -293,7 +326,7 @@ module ActiveRecord
|
|
293
326
|
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
|
294
327
|
# in Base is replaced with this when the validations module is mixed in, which it is by default.
|
295
328
|
def update_attribute_with_validation_skipping(name, value)
|
296
|
-
|
329
|
+
self[name] = value
|
297
330
|
save(false)
|
298
331
|
end
|
299
332
|
|
@@ -0,0 +1,305 @@
|
|
1
|
+
#
|
2
|
+
# mysq411.rb - 0.1 - Matt Mower <self@mattmower.com>
|
3
|
+
#
|
4
|
+
# The native Ruby MySQL client (mysql.rb) by Tomita Masahiro does not (yet) handle the new MySQL
|
5
|
+
# protocol introduced in MySQL 4.1.1. This protocol introduces a new authentication scheme as
|
6
|
+
# well as modifications to the client/server exchanges themselves.
|
7
|
+
#
|
8
|
+
# mysql411.rb modifies the Mysql class to add MySQL 4.1.x support. It modifies the connection
|
9
|
+
# algorithm to detect a 4.1.1 server and respond with the new authentication scheme, otherwise using
|
10
|
+
# the original one. Similarly for the changes to packet structures and field definitions, etc...
|
11
|
+
#
|
12
|
+
# It redefines serveral methods which behave differently depending upon the server context. The
|
13
|
+
# way I have implemented this is to alias the old method, create a new alternative method, and redefine
|
14
|
+
# the original method as a selector which calls the appropriate method based upon the server version.
|
15
|
+
# There may have been a neater way to do this.
|
16
|
+
#
|
17
|
+
# In general I've tried not to change the original code any more than necessary, i.e. even where I
|
18
|
+
# redefine a method I have made the smallest number of changes possible, rather than rewriting from
|
19
|
+
# scratch.
|
20
|
+
#
|
21
|
+
# *Caveat Lector* This code passes all current ActiveRecord unit tests however this is no guarantee that
|
22
|
+
# full & correct MySQL 4.1 support has been achieved.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'digest/sha1'
|
26
|
+
|
27
|
+
#
|
28
|
+
# Extend the Mysql class to work with MySQL 4.1.1+ servers. After version
|
29
|
+
# 4.1.1 the password hashing function (and some other connection details) have
|
30
|
+
# changed rendering the previous Mysql class unable to connect:
|
31
|
+
#
|
32
|
+
#
|
33
|
+
|
34
|
+
class Mysql
|
35
|
+
CLIENT_PROTOCOL_41 = 512
|
36
|
+
CLIENT_SECURE_CONNECTION = 32768
|
37
|
+
|
38
|
+
def real_connect( host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil )
|
39
|
+
@server_status = SERVER_STATUS_AUTOCOMMIT
|
40
|
+
|
41
|
+
if( host == nil || host == "localhost" ) && defined? UNIXSocket
|
42
|
+
unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
|
43
|
+
sock = UNIXSocket::new( unix_socket )
|
44
|
+
@host_info = Error::err( Error::CR_LOCALHOST_CONNECTION )
|
45
|
+
@unix_socket = unix_socket
|
46
|
+
else
|
47
|
+
sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
|
48
|
+
@host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
|
49
|
+
end
|
50
|
+
|
51
|
+
@host = host ? host.dup : nil
|
52
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
|
53
|
+
@net = Net::new sock
|
54
|
+
|
55
|
+
a = read
|
56
|
+
|
57
|
+
@protocol_version = a.slice!(0)
|
58
|
+
@server_version, a = a.split(/\0/,2)
|
59
|
+
|
60
|
+
# Store the version number components for speedy comparison
|
61
|
+
version, ostag = @server_version.split( /-/, 2 )
|
62
|
+
@major_ver, @minor_ver, @revision_num = version.split( /\./ ).map { |v| v.to_i }
|
63
|
+
|
64
|
+
@thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
|
65
|
+
if a.size >= 2 then
|
66
|
+
@server_capabilities, = a.slice!(0,2).unpack("v")
|
67
|
+
end
|
68
|
+
if a.size >= 16 then
|
69
|
+
@server_language, @server_status = a.unpack("cv")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the flags we'll send back to the server
|
73
|
+
flag = 0 if flag == nil
|
74
|
+
flag |= @client_flag | CLIENT_CAPABILITIES
|
75
|
+
flag |= CLIENT_CONNECT_WITH_DB if db
|
76
|
+
|
77
|
+
if version_meets_minimum?( 4, 1, 1 )
|
78
|
+
# In 4.1.1+ the seed comes in two parts which must be combined
|
79
|
+
a.slice!( 0, 16 )
|
80
|
+
seed_part_2 = a.slice!( 0, 12 );
|
81
|
+
@scramble_buff << seed_part_2
|
82
|
+
|
83
|
+
flag |= CLIENT_FOUND_ROWS
|
84
|
+
flag |= CLIENT_PROTOCOL_41
|
85
|
+
flag |= CLIENT_SECURE_CONNECTION if @server_capabilities & CLIENT_SECURE_CONNECTION;
|
86
|
+
|
87
|
+
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
|
88
|
+
@db = db.dup
|
89
|
+
end
|
90
|
+
|
91
|
+
scrambled_password = scramble411( passwd, @scramble_buff, @protocol_version==9 )
|
92
|
+
data = make_client_auth_packet_41( flag, user, scrambled_password, db )
|
93
|
+
else
|
94
|
+
scrambled_password = scramble( passwd, @scramble_buff, @protocol_version == 9 )
|
95
|
+
data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scrambled_password
|
96
|
+
if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
|
97
|
+
data << "\0"+db
|
98
|
+
@db = db.dup
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
write data
|
103
|
+
read
|
104
|
+
self
|
105
|
+
end
|
106
|
+
alias :connect :real_connect
|
107
|
+
|
108
|
+
# Pack the authentication information into depending upon whether an initial database has
|
109
|
+
# been specified
|
110
|
+
def make_client_auth_packet_41( flag, user, password, db )
|
111
|
+
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
|
112
|
+
template = "VVcx23a#{user.size+1}cA#{password.size}a#{db.size+1}"
|
113
|
+
else
|
114
|
+
template = "VVcx23a#{user.size+1}cA#{password.size}x"
|
115
|
+
end
|
116
|
+
|
117
|
+
[ flag, @max_allowed_packet, @server_language, user, password.size, password, db ].pack( template )
|
118
|
+
end
|
119
|
+
|
120
|
+
def version_meets_minimum?( major, minor, revision )
|
121
|
+
@major_ver >= major && @minor_ver >= minor && @revision_num >= revision
|
122
|
+
end
|
123
|
+
|
124
|
+
# SERVER: public_seed=create_random_string()
|
125
|
+
# send(public_seed)
|
126
|
+
#
|
127
|
+
# CLIENT: recv(public_seed)
|
128
|
+
# hash_stage1=sha1("password")
|
129
|
+
# hash_stage2=sha1(hash_stage1)
|
130
|
+
# reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
|
131
|
+
#
|
132
|
+
# #this three steps are done in scramble()
|
133
|
+
#
|
134
|
+
# send(reply)
|
135
|
+
#
|
136
|
+
#
|
137
|
+
# SERVER: recv(reply)
|
138
|
+
# hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
|
139
|
+
# candidate_hash2=sha1(hash_stage1)
|
140
|
+
# check(candidate_hash2==hash_stage2)
|
141
|
+
def scramble411( password, seed, old_ver )
|
142
|
+
raise "old version password is not implemented" if old_ver
|
143
|
+
|
144
|
+
# print "Seed Bytes = "
|
145
|
+
# seed.each_byte { |b| print "0x#{b.to_s( 16 )}, " }
|
146
|
+
# puts
|
147
|
+
|
148
|
+
stage1 = Digest::SHA1.digest( password )
|
149
|
+
stage2 = Digest::SHA1.digest( stage1 )
|
150
|
+
|
151
|
+
dgst = Digest::SHA1.new
|
152
|
+
dgst << seed
|
153
|
+
dgst << stage2
|
154
|
+
stage3 = dgst.digest
|
155
|
+
|
156
|
+
# stage1.zip( stage3 ).map { |a, b| (a ^ b).chr }.join
|
157
|
+
scrambled = ( 0 ... stage3.size ).map { |i| stage3[i] ^ stage1[i] }
|
158
|
+
scrambled = scrambled.map { |x| x.chr }
|
159
|
+
scrambled.join
|
160
|
+
end
|
161
|
+
|
162
|
+
def change_user(user="", passwd="", db="")
|
163
|
+
scrambled_password = version_meets_minimum?( 4, 1, 1 ) ? scramble411( passwd, @scramble_buff, @protocol_version==9 ) : scramble( passwd, @scramble_buff, @protocol_version==9 )
|
164
|
+
data = user+"\0"+scrambled_password+"\0"+db
|
165
|
+
command COM_CHANGE_USER, data
|
166
|
+
@user = user
|
167
|
+
@passwd = passwd
|
168
|
+
@db = db
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# The 4.1 protocol changed the length of the END packet
|
173
|
+
#
|
174
|
+
alias_method :old_read_one_row, :read_one_row
|
175
|
+
|
176
|
+
def read_one_row( field_count )
|
177
|
+
if version_meets_minimum?( 4, 1, 1 )
|
178
|
+
read_one_row_41( field_count )
|
179
|
+
else
|
180
|
+
old_read_one_row( field_count )
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def read_one_row_41( field_count )
|
185
|
+
data = read
|
186
|
+
return if data[0] == 254 and data.length < 9
|
187
|
+
rec = []
|
188
|
+
field_count.times do
|
189
|
+
len = get_length data
|
190
|
+
if len == nil then
|
191
|
+
rec << len
|
192
|
+
else
|
193
|
+
rec << data.slice!(0,len)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
rec
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# The 4.1 protocol changed the length of the END packet
|
201
|
+
#
|
202
|
+
alias_method :old_skip_result, :skip_result
|
203
|
+
|
204
|
+
def skip_result
|
205
|
+
if version_meets_minimum?( 4, 1, 1 )
|
206
|
+
skip_result_41
|
207
|
+
else
|
208
|
+
old_skip_result
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def skip_result_41()
|
213
|
+
if @status == :STATUS_USE_RESULT then
|
214
|
+
loop do
|
215
|
+
data = read
|
216
|
+
break if data[0] == 254 and data.length == 1
|
217
|
+
end
|
218
|
+
@status = :STATUS_READY
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# The field description structure is changed for the 4.1 protocol passing
|
223
|
+
# more data and a different packing form. NOTE: The 4.1 protocol now passes
|
224
|
+
# back a "catalog" name for each field which is a new feature. Since AR has
|
225
|
+
# nowhere to put it I'm throwing it away. Possibly this is not the best
|
226
|
+
# idea?
|
227
|
+
#
|
228
|
+
alias_method :old_unpack_fields, :unpack_fields
|
229
|
+
|
230
|
+
def unpack_fields( data, long_flag_protocol )
|
231
|
+
if version_meets_minimum?( 4, 1, 1 )
|
232
|
+
unpack_fields_41( data, long_flag_protocol )
|
233
|
+
else
|
234
|
+
old_unpack_fields( data, long_flag_protocol )
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def unpack_fields_41( data, long_flag_protocol )
|
239
|
+
ret = []
|
240
|
+
|
241
|
+
data.each do |f|
|
242
|
+
catalog_name = f[0]
|
243
|
+
database_name = f[1]
|
244
|
+
table_name_alias = f[2]
|
245
|
+
table_name = f[3]
|
246
|
+
column_name_alias = f[4]
|
247
|
+
column_name = f[5]
|
248
|
+
|
249
|
+
charset = f[6][0] + f[6][1]*256
|
250
|
+
length = f[6][2] + f[6][3]*256 + f[6][4]*256*256 + f[6][5]*256*256*256
|
251
|
+
type = f[6][6]
|
252
|
+
flags = f[6][7] + f[6][8]*256
|
253
|
+
decimals = f[6][9]
|
254
|
+
def_value = f[7]
|
255
|
+
max_length = 0
|
256
|
+
|
257
|
+
ret << Field::new(table_name, table_name, column_name, length, type, flags, decimals, def_value, max_length)
|
258
|
+
end
|
259
|
+
ret
|
260
|
+
end
|
261
|
+
|
262
|
+
# In this instance the read_query_result method in mysql is bound to read 5 field parameters which
|
263
|
+
# is expanded to 7 in the 4.1 protocol. So in this case we redefine this entire method in order
|
264
|
+
# to write "read_rows 7" instead of "read_rows 5"!
|
265
|
+
#
|
266
|
+
alias_method :old_read_query_result, :read_query_result
|
267
|
+
|
268
|
+
def read_query_result
|
269
|
+
if version_meets_minimum?( 4, 1, 1 )
|
270
|
+
read_query_result_41
|
271
|
+
else
|
272
|
+
old_read_query_result
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def read_query_result_41
|
277
|
+
data = read
|
278
|
+
@field_count = get_length(data)
|
279
|
+
if @field_count == nil then # LOAD DATA LOCAL INFILE
|
280
|
+
File::open(data) do |f|
|
281
|
+
write f.read
|
282
|
+
end
|
283
|
+
write "" # mark EOF
|
284
|
+
data = read
|
285
|
+
@field_count = get_length(data)
|
286
|
+
end
|
287
|
+
if @field_count == 0 then
|
288
|
+
@affected_rows = get_length(data, true)
|
289
|
+
@insert_id = get_length(data, true)
|
290
|
+
if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
|
291
|
+
a = data.slice!(0,2)
|
292
|
+
@server_status = a[0]+a[1]*256
|
293
|
+
end
|
294
|
+
if data.size > 0 and get_length(data) then
|
295
|
+
@info = data
|
296
|
+
end
|
297
|
+
else
|
298
|
+
@extra_info = get_length(data, true)
|
299
|
+
fields = read_rows 7
|
300
|
+
@fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
|
301
|
+
@status = :STATUS_GET_RESULT
|
302
|
+
end
|
303
|
+
self
|
304
|
+
end
|
305
|
+
end
|
data/rakefile
CHANGED
@@ -8,7 +8,7 @@ require 'rake/contrib/rubyforgepublisher'
|
|
8
8
|
|
9
9
|
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
10
10
|
PKG_NAME = 'activerecord'
|
11
|
-
PKG_VERSION = '1.
|
11
|
+
PKG_VERSION = '1.5.0' + PKG_BUILD
|
12
12
|
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
13
13
|
|
14
14
|
PKG_FILES = FileList[
|
@@ -17,7 +17,7 @@ PKG_FILES = FileList[
|
|
17
17
|
|
18
18
|
|
19
19
|
desc "Default Task"
|
20
|
-
task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postgresql ]
|
20
|
+
task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_sqlite3, :test_postgresql ]
|
21
21
|
|
22
22
|
# Run the unit tests
|
23
23
|
|
@@ -45,6 +45,12 @@ Rake::TestTask.new("test_sqlite") { |t|
|
|
45
45
|
t.verbose = true
|
46
46
|
}
|
47
47
|
|
48
|
+
Rake::TestTask.new("test_sqlite3") { |t|
|
49
|
+
t.libs << "test" << "test/connections/native_sqlite3"
|
50
|
+
t.pattern = 'test/*_test.rb'
|
51
|
+
t.verbose = true
|
52
|
+
}
|
53
|
+
|
48
54
|
Rake::TestTask.new("test_sqlserver") { |t|
|
49
55
|
t.libs << "test" << "test/connections/native_sqlserver"
|
50
56
|
t.pattern = 'test/*_test.rb'
|
@@ -99,6 +105,9 @@ spec = Gem::Specification.new do |s|
|
|
99
105
|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
100
106
|
end
|
101
107
|
s.files.delete "test/fixtures/fixture_database.sqlite"
|
108
|
+
s.files.delete "test/fixtures/fixture_database_2.sqlite"
|
109
|
+
s.files.delete "test/fixtures/fixture_database.sqlite3"
|
110
|
+
s.files.delete "test/fixtures/fixture_database_2.sqlite3"
|
102
111
|
s.require_path = 'lib'
|
103
112
|
s.autorequire = 'active_record'
|
104
113
|
|
data/test/abstract_unit.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
-
# $:.unshift(File.dirname(__FILE__) + '/fixtures')
|
3
2
|
|
4
3
|
require 'test/unit'
|
5
4
|
require 'active_record'
|
@@ -18,4 +17,4 @@ class Test::Unit::TestCase #:nodoc:
|
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
21
|
-
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
20
|
+
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
data/test/associations_test.rb
CHANGED
@@ -5,6 +5,7 @@ require 'fixtures/project'
|
|
5
5
|
require 'fixtures/company'
|
6
6
|
require 'fixtures/topic'
|
7
7
|
require 'fixtures/reply'
|
8
|
+
require 'fixtures/computer'
|
8
9
|
|
9
10
|
# Can't declare new classes in test case methods, so tests before that
|
10
11
|
bad_collection_keys = false
|
@@ -18,19 +19,19 @@ raise "ActiveRecord should have barked on bad collection keys" unless bad_collec
|
|
18
19
|
|
19
20
|
class AssociationsTest < Test::Unit::TestCase
|
20
21
|
def setup
|
21
|
-
create_fixtures "accounts", "companies", "developers", "projects", "developers_projects"
|
22
|
+
create_fixtures "accounts", "companies", "developers", "projects", "developers_projects", "computers"
|
22
23
|
@signals37 = Firm.find(1)
|
23
24
|
end
|
24
25
|
|
25
26
|
def test_force_reload
|
26
|
-
firm = Firm.new
|
27
|
+
firm = Firm.new("name" => "A New Firm, Inc")
|
27
28
|
firm.save
|
28
29
|
firm.clients.each {|c|} # forcing to load all clients
|
29
30
|
assert firm.clients.empty?, "New firm shouldn't have client objects"
|
30
31
|
assert !firm.has_clients?, "New firm shouldn't have clients"
|
31
32
|
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
|
32
33
|
|
33
|
-
client = Client.new("firm_id" => firm.id)
|
34
|
+
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
|
34
35
|
client.save
|
35
36
|
|
36
37
|
assert firm.clients.empty?, "New firm should have cached no client objects"
|
@@ -71,12 +72,6 @@ class HasOneAssociationsTest < Test::Unit::TestCase
|
|
71
72
|
def test_has_one
|
72
73
|
assert_equal @signals37.account, Account.find(1)
|
73
74
|
assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
|
74
|
-
assert @signals37.has_account?, "37signals should have an account"
|
75
|
-
assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
|
76
|
-
assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
|
77
|
-
|
78
|
-
assert !Account.find(2).has_firm?, "Unknown isn't linked"
|
79
|
-
assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
|
80
75
|
end
|
81
76
|
|
82
77
|
def test_type_mismatch
|
@@ -99,43 +94,105 @@ class HasOneAssociationsTest < Test::Unit::TestCase
|
|
99
94
|
assert_nil Account.find(old_account_id).firm_id
|
100
95
|
end
|
101
96
|
|
97
|
+
def test_dependence
|
98
|
+
firm = Firm.find(1)
|
99
|
+
assert !firm.account.nil?
|
100
|
+
firm.destroy
|
101
|
+
assert_equal 1, Account.find_all.length
|
102
|
+
end
|
103
|
+
|
102
104
|
def test_build
|
103
105
|
firm = Firm.new("name" => "GlobalMegaCorp")
|
104
106
|
firm.save
|
105
|
-
|
106
|
-
account = firm.
|
107
|
+
|
108
|
+
account = firm.account.build("credit_limit" => 1000)
|
109
|
+
assert_equal account, firm.account
|
107
110
|
assert account.save
|
108
111
|
assert_equal account, firm.account
|
109
112
|
end
|
110
113
|
|
114
|
+
def test_build_before_child_saved
|
115
|
+
firm = Firm.find(1)
|
116
|
+
|
117
|
+
account = firm.account.build("credit_limit" => 1000)
|
118
|
+
assert_equal account, firm.account
|
119
|
+
assert account.new_record?
|
120
|
+
assert firm.save
|
121
|
+
assert_equal account, firm.account
|
122
|
+
assert !account.new_record?
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_build_before_either_saved
|
126
|
+
firm = Firm.new("name" => "GlobalMegaCorp")
|
127
|
+
|
128
|
+
account = firm.account.build("credit_limit" => 1000)
|
129
|
+
assert_equal account, firm.account
|
130
|
+
assert account.new_record?
|
131
|
+
assert firm.save
|
132
|
+
assert_equal account, firm.account
|
133
|
+
assert !account.new_record?
|
134
|
+
end
|
135
|
+
|
111
136
|
def test_failing_build_association
|
112
137
|
firm = Firm.new("name" => "GlobalMegaCorp")
|
113
138
|
firm.save
|
114
139
|
|
115
|
-
account = firm.
|
140
|
+
account = firm.account.build
|
141
|
+
assert_equal account, firm.account
|
116
142
|
assert !account.save
|
143
|
+
assert_equal account, firm.account
|
117
144
|
assert_equal "can't be empty", account.errors.on("credit_limit")
|
118
145
|
end
|
119
146
|
|
120
147
|
def test_create
|
121
148
|
firm = Firm.new("name" => "GlobalMegaCorp")
|
122
149
|
firm.save
|
123
|
-
assert_equal firm.
|
150
|
+
assert_equal firm.account.create("credit_limit" => 1000), firm.account
|
124
151
|
end
|
125
|
-
|
126
|
-
def
|
127
|
-
firm = Firm.
|
128
|
-
|
129
|
-
firm.destroy
|
130
|
-
assert_equal 1, Account.find_all.length
|
152
|
+
|
153
|
+
def test_create_before_save
|
154
|
+
firm = Firm.new("name" => "GlobalMegaCorp")
|
155
|
+
assert_equal firm.account.create("credit_limit" => 1000), firm.account
|
131
156
|
end
|
132
157
|
|
133
158
|
def test_dependence_with_missing_association
|
134
159
|
Account.destroy_all
|
135
|
-
firm = Firm.find(1)
|
136
|
-
assert
|
160
|
+
firm = Firm.find(1)
|
161
|
+
assert firm.account.nil?
|
137
162
|
firm.destroy
|
138
163
|
end
|
164
|
+
|
165
|
+
def test_assignment_before_parent_saved
|
166
|
+
firm = Firm.new("name" => "GlobalMegaCorp")
|
167
|
+
firm.account = a = Account.find(1)
|
168
|
+
assert firm.new_record?
|
169
|
+
assert_equal a, firm.account
|
170
|
+
assert firm.save
|
171
|
+
assert_equal a, firm.account
|
172
|
+
assert_equal a, firm.account(true)
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_assignment_before_child_saved
|
176
|
+
firm = Firm.find(1)
|
177
|
+
firm.account = a = Account.new("credit_limit" => 1000)
|
178
|
+
assert !a.new_record?
|
179
|
+
assert_equal a, firm.account
|
180
|
+
assert_equal a, firm.account
|
181
|
+
assert_equal a, firm.account(true)
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_assignment_before_either_saved
|
185
|
+
firm = Firm.new("name" => "GlobalMegaCorp")
|
186
|
+
firm.account = a = Account.new("credit_limit" => 1000)
|
187
|
+
assert firm.new_record?
|
188
|
+
assert a.new_record?
|
189
|
+
assert_equal a, firm.account
|
190
|
+
assert firm.save
|
191
|
+
assert !firm.new_record?
|
192
|
+
assert !a.new_record?
|
193
|
+
assert_equal a, firm.account
|
194
|
+
assert_equal a, firm.account(true)
|
195
|
+
end
|
139
196
|
end
|
140
197
|
|
141
198
|
|
@@ -256,16 +313,72 @@ class HasManyAssociationsTest < Test::Unit::TestCase
|
|
256
313
|
assert_equal 3, @signals37.clients_of_firm(true).size
|
257
314
|
end
|
258
315
|
|
316
|
+
def test_adding_before_save
|
317
|
+
no_of_firms = Firm.count
|
318
|
+
no_of_clients = Client.count
|
319
|
+
new_firm = Firm.new("name" => "A New Firm, Inc")
|
320
|
+
new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
|
321
|
+
new_firm.clients_of_firm << (c = Client.new("name" => "Apple"))
|
322
|
+
assert new_firm.new_record?
|
323
|
+
assert c.new_record?
|
324
|
+
assert_equal 2, new_firm.clients_of_firm.size
|
325
|
+
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
|
326
|
+
assert_equal no_of_clients, Client.count # Clients were not saved to database.
|
327
|
+
assert new_firm.save
|
328
|
+
assert !new_firm.new_record?
|
329
|
+
assert !c.new_record?
|
330
|
+
assert_equal new_firm, c.firm
|
331
|
+
assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
|
332
|
+
assert_equal no_of_clients+2, Client.count # Clients were saved to database.
|
333
|
+
assert_equal 2, new_firm.clients_of_firm.size
|
334
|
+
assert_equal 2, new_firm.clients_of_firm(true).size
|
335
|
+
end
|
336
|
+
|
337
|
+
def test_invalid_adding
|
338
|
+
firm = Firm.find(1)
|
339
|
+
assert !(firm.clients_of_firm << c = Client.new)
|
340
|
+
assert c.new_record?
|
341
|
+
assert !firm.save
|
342
|
+
assert c.new_record?
|
343
|
+
end
|
344
|
+
|
345
|
+
def test_invalid_adding_before_save
|
346
|
+
no_of_firms = Firm.count
|
347
|
+
no_of_clients = Client.count
|
348
|
+
new_firm = Firm.new("name" => "A New Firm, Inc")
|
349
|
+
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
|
350
|
+
assert c.new_record?
|
351
|
+
assert !c.valid?
|
352
|
+
assert new_firm.valid?
|
353
|
+
assert !new_firm.save
|
354
|
+
assert c.new_record?
|
355
|
+
assert new_firm.new_record?
|
356
|
+
end
|
357
|
+
|
259
358
|
def test_build
|
260
359
|
new_client = @signals37.clients_of_firm.build("name" => "Another Client")
|
261
360
|
assert_equal "Another Client", new_client.name
|
262
|
-
assert new_client.
|
361
|
+
assert new_client.new_record?
|
362
|
+
assert_equal new_client, @signals37.clients_of_firm.last
|
363
|
+
assert @signals37.save
|
364
|
+
assert !new_client.new_record?
|
263
365
|
assert_equal 2, @signals37.clients_of_firm(true).size
|
264
366
|
end
|
367
|
+
|
368
|
+
def test_invalid_build
|
369
|
+
new_client = @signals37.clients_of_firm.build
|
370
|
+
assert new_client.new_record?
|
371
|
+
assert !new_client.valid?
|
372
|
+
assert_equal new_client, @signals37.clients_of_firm.last
|
373
|
+
assert !@signals37.save
|
374
|
+
assert new_client.new_record?
|
375
|
+
assert_equal 1, @signals37.clients_of_firm(true).size
|
376
|
+
end
|
265
377
|
|
266
378
|
def test_create
|
267
379
|
force_signal37_to_load_all_clients_of_firm
|
268
380
|
new_client = @signals37.clients_of_firm.create("name" => "Another Client")
|
381
|
+
assert !new_client.new_record?
|
269
382
|
assert_equal new_client, @signals37.clients_of_firm.last
|
270
383
|
assert_equal new_client, @signals37.clients_of_firm(true).last
|
271
384
|
end
|
@@ -277,6 +390,14 @@ class HasManyAssociationsTest < Test::Unit::TestCase
|
|
277
390
|
assert_equal 0, @signals37.clients_of_firm(true).size
|
278
391
|
end
|
279
392
|
|
393
|
+
def test_deleting_before_save
|
394
|
+
new_firm = Firm.new("name" => "A New Firm, Inc.")
|
395
|
+
new_client = new_firm.clients_of_firm.build("name" => "Another Client")
|
396
|
+
assert_equal 1, new_firm.clients_of_firm.size
|
397
|
+
new_firm.clients_of_firm.delete(new_client)
|
398
|
+
assert_equal 0, new_firm.clients_of_firm.size
|
399
|
+
end
|
400
|
+
|
280
401
|
def test_deleting_a_collection
|
281
402
|
force_signal37_to_load_all_clients_of_firm
|
282
403
|
@signals37.clients_of_firm.create("name" => "Another Client")
|
@@ -366,8 +487,10 @@ class HasManyAssociationsTest < Test::Unit::TestCase
|
|
366
487
|
end
|
367
488
|
|
368
489
|
class BelongsToAssociationsTest < Test::Unit::TestCase
|
490
|
+
fixtures :accounts, :companies, :developers, :projects, :topics
|
491
|
+
|
369
492
|
def setup
|
370
|
-
create_fixtures "
|
493
|
+
create_fixtures "developers_projects"
|
371
494
|
@signals37 = Firm.find(1)
|
372
495
|
end
|
373
496
|
|
@@ -418,6 +541,62 @@ class BelongsToAssociationsTest < Test::Unit::TestCase
|
|
418
541
|
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
|
419
542
|
end
|
420
543
|
|
544
|
+
def test_assignment_before_parent_saved
|
545
|
+
client = Client.find_first
|
546
|
+
apple = Firm.new("name" => "Apple")
|
547
|
+
client.firm = apple
|
548
|
+
assert_equal apple, client.firm
|
549
|
+
assert apple.new_record?
|
550
|
+
assert client.save
|
551
|
+
assert apple.save
|
552
|
+
assert !apple.new_record?
|
553
|
+
assert_equal apple, client.firm
|
554
|
+
assert_equal apple, client.firm(true)
|
555
|
+
end
|
556
|
+
|
557
|
+
def test_assignment_before_child_saved
|
558
|
+
final_cut = Client.new("name" => "Final Cut")
|
559
|
+
firm = Firm.find(1)
|
560
|
+
final_cut.firm = firm
|
561
|
+
assert final_cut.new_record?
|
562
|
+
assert final_cut.save
|
563
|
+
assert !final_cut.new_record?
|
564
|
+
assert !firm.new_record?
|
565
|
+
assert_equal firm, final_cut.firm
|
566
|
+
assert_equal firm, final_cut.firm(true)
|
567
|
+
end
|
568
|
+
|
569
|
+
def test_assignment_before_either_saved
|
570
|
+
final_cut = Client.new("name" => "Final Cut")
|
571
|
+
apple = Firm.new("name" => "Apple")
|
572
|
+
final_cut.firm = apple
|
573
|
+
assert final_cut.new_record?
|
574
|
+
assert apple.new_record?
|
575
|
+
assert final_cut.save
|
576
|
+
assert !final_cut.new_record?
|
577
|
+
assert !apple.new_record?
|
578
|
+
assert_equal apple, final_cut.firm
|
579
|
+
assert_equal apple, final_cut.firm(true)
|
580
|
+
end
|
581
|
+
|
582
|
+
def test_new_record_with_foreign_key_but_no_object
|
583
|
+
c = Client.new("firm_id" => 1)
|
584
|
+
assert_equal @first_firm, c.firm_with_basic_id
|
585
|
+
end
|
586
|
+
|
587
|
+
def test_forgetting_the_load_when_foreign_key_enters_late
|
588
|
+
c = Client.new
|
589
|
+
assert_nil c.firm_with_basic_id
|
590
|
+
|
591
|
+
c.firm_id = 1
|
592
|
+
assert_equal @first_firm, c.firm_with_basic_id
|
593
|
+
end
|
594
|
+
|
595
|
+
def test_field_name_same_as_foreign_key
|
596
|
+
computer = Computer.find 1
|
597
|
+
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up"
|
598
|
+
end
|
599
|
+
|
421
600
|
def xtest_counter_cache
|
422
601
|
apple = Firm.create("name" => "Apple")
|
423
602
|
final_cut = apple.clients.create("name" => "Final Cut")
|
@@ -508,6 +687,38 @@ class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
|
|
508
687
|
assert_equal 2, aridridel.projects.size
|
509
688
|
assert_equal 2, aridridel.projects(true).size
|
510
689
|
end
|
690
|
+
|
691
|
+
def test_habtm_adding_before_save
|
692
|
+
no_of_devels = Developer.count
|
693
|
+
no_of_projects = Project.count
|
694
|
+
aridridel = Developer.new("name" => "Aridridel")
|
695
|
+
aridridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
|
696
|
+
assert aridridel.new_record?
|
697
|
+
assert p.new_record?
|
698
|
+
assert aridridel.save
|
699
|
+
assert !aridridel.new_record?
|
700
|
+
assert_equal no_of_devels+1, Developer.count
|
701
|
+
assert_equal no_of_projects+1, Project.count
|
702
|
+
assert_equal 2, aridridel.projects.size
|
703
|
+
assert_equal 2, aridridel.projects(true).size
|
704
|
+
end
|
705
|
+
|
706
|
+
def test_build
|
707
|
+
devel = Developer.find(1)
|
708
|
+
proj = devel.projects.build("name" => "Projekt")
|
709
|
+
assert_equal devel.projects.last, proj
|
710
|
+
assert proj.new_record?
|
711
|
+
devel.save
|
712
|
+
assert !proj.new_record?
|
713
|
+
assert_equal devel.projects.last, proj
|
714
|
+
end
|
715
|
+
|
716
|
+
def test_create
|
717
|
+
devel = Developer.find(1)
|
718
|
+
proj = devel.projects.create("name" => "Projekt")
|
719
|
+
assert_equal devel.projects.last, proj
|
720
|
+
assert !proj.new_record?
|
721
|
+
end
|
511
722
|
|
512
723
|
def test_uniq_after_the_fact
|
513
724
|
@developers["jamis"].find.projects << @projects["active_record"].find
|