activerecord 1.7.0 → 1.8.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 (39) hide show
  1. data/CHANGELOG +55 -0
  2. data/lib/active_record.rb +5 -7
  3. data/lib/active_record/aggregations.rb +2 -1
  4. data/lib/active_record/associations.rb +2 -1
  5. data/lib/active_record/associations/association_proxy.rb +4 -0
  6. data/lib/active_record/associations/has_many_association.rb +6 -4
  7. data/lib/active_record/associations/has_one_association.rb +7 -2
  8. data/lib/active_record/base.rb +36 -5
  9. data/lib/active_record/connection_adapters/abstract_adapter.rb +58 -10
  10. data/lib/active_record/connection_adapters/mysql_adapter.rb +30 -9
  11. data/lib/active_record/connection_adapters/postgresql_adapter.rb +3 -2
  12. data/lib/active_record/connection_adapters/sqlite_adapter.rb +17 -0
  13. data/lib/active_record/fixtures.rb +108 -31
  14. data/lib/active_record/migration.rb +94 -0
  15. data/lib/active_record/reflection.rb +10 -4
  16. data/lib/active_record/validations.rb +76 -42
  17. data/rakefile +2 -2
  18. data/test/aaa_create_tables_test.rb +4 -4
  19. data/test/aggregations_test.rb +18 -4
  20. data/test/associations_test.rb +2 -1
  21. data/test/base_test.rb +10 -0
  22. data/test/fixtures/company.rb +1 -1
  23. data/test/fixtures/customer.rb +17 -0
  24. data/test/fixtures/customers.yml +1 -0
  25. data/test/fixtures/db_definitions/db2.sql +1 -0
  26. data/test/fixtures/db_definitions/mysql.sql +1 -0
  27. data/test/fixtures/db_definitions/oci.sql +1 -0
  28. data/test/fixtures/db_definitions/postgresql.sql +1 -0
  29. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  30. data/test/fixtures/db_definitions/sqlserver.sql +1 -0
  31. data/test/fixtures/fixture_database.sqlite +0 -0
  32. data/test/fixtures/fixture_database_2.sqlite +0 -0
  33. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  34. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  35. data/test/fixtures_test.rb +30 -1
  36. data/test/migration_mysql.rb +104 -0
  37. data/test/reflection_test.rb +8 -4
  38. data/test/validations_test.rb +38 -0
  39. metadata +9 -4
@@ -1,8 +1,6 @@
1
1
  require 'erb'
2
2
  require 'yaml'
3
3
  require 'csv'
4
- require 'active_support/class_inheritable_attributes'
5
- require 'active_support/inflector'
6
4
 
7
5
  # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours:
8
6
  #
@@ -140,16 +138,46 @@ require 'active_support/inflector'
140
138
  # This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
141
139
  # sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
142
140
  # is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
141
+ #
142
+ # = Transactional fixtures
143
+ #
144
+ # TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
145
+ # They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
146
+ #
147
+ # class FooTest < Test::Unit::TestCase
148
+ # self.use_transactional_fixtures = true
149
+ # self.use_instantiated_fixtures = false
150
+ #
151
+ # fixtures :foos
152
+ #
153
+ # def test_godzilla
154
+ # assert !Foo.find_all.emtpy?
155
+ # Foo.destroy_all
156
+ # assert Foo.find_all.emtpy?
157
+ # end
158
+ #
159
+ # def test_godzilla_aftermath
160
+ # assert !Foo.find_all.emtpy?
161
+ # end
162
+ # end
163
+ #
164
+ # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
165
+ # then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
166
+ #
167
+ # When *not* to use transactional fixtures:
168
+ # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
169
+ # particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
170
+ # the results of your transaction until Active Record supports nested transactions or savepoints (in progress.)
171
+ # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
172
+ # Use InnoDB, MaxDB, or NDB instead.
143
173
  class Fixtures < Hash
144
174
  DEFAULT_FILTER_RE = /\.ya?ml$/
145
175
 
146
- def self.instantiate_fixtures(object, fixtures_directory, *table_names)
147
- [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
148
- object.instance_variable_set "@#{table_names[idx]}", fixtures
149
- fixtures.each do |name, fixture|
150
- if model = fixture.find
151
- object.instance_variable_set "@#{name}", model
152
- end
176
+ def self.instantiate_fixtures(object, table_name, fixtures)
177
+ object.instance_variable_set "@#{table_name}", fixtures
178
+ fixtures.each do |name, fixture|
179
+ if model = fixture.find
180
+ object.instance_variable_set "@#{name}", model
153
181
  end
154
182
  end
155
183
  end
@@ -219,7 +247,7 @@ class Fixtures < Hash
219
247
  yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
220
248
  yaml.each { |name, data| self[name] = Fixture.new(data, @class_name) } if yaml
221
249
  rescue Exception=>boom
222
- raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}"
250
+ raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html"
223
251
  end
224
252
  elsif File.file?(csv_file_path)
225
253
  # CSV fixtures
@@ -324,51 +352,100 @@ class Fixture #:nodoc:
324
352
  end
325
353
  end
326
354
 
327
- module Test#:nodoc:
328
- module Unit#:nodoc:
355
+ module Test #:nodoc:
356
+ module Unit #:nodoc:
329
357
  class TestCase #:nodoc:
330
358
  include ClassInheritableAttributes
331
359
 
332
360
  cattr_accessor :fixture_path
333
- cattr_accessor :fixture_table_names
361
+ class_inheritable_accessor :fixture_table_names
362
+ class_inheritable_accessor :use_transactional_fixtures
363
+ class_inheritable_accessor :use_instantiated_fixtures
364
+
365
+ self.fixture_table_names = []
366
+ self.use_transactional_fixtures = false
367
+ self.use_instantiated_fixtures = true
334
368
 
335
369
  def self.fixtures(*table_names)
336
- require_fixture_classes(table_names)
337
- write_inheritable_attribute("fixture_table_names", table_names)
370
+ self.fixture_table_names = table_names.flatten
371
+ require_fixture_classes
338
372
  end
339
373
 
340
- def self.require_fixture_classes(table_names)
341
- table_names.each do |table_name|
374
+ def self.require_fixture_classes
375
+ fixture_table_names.each do |table_name|
342
376
  begin
343
- require(Inflector.singularize(table_name.to_s))
377
+ require Inflector.singularize(table_name.to_s)
344
378
  rescue LoadError
345
- # Let's hope the developer is included it himself
379
+ # Let's hope the developer has included it himself
346
380
  end
347
381
  end
348
382
  end
349
383
 
350
- def setup
351
- instantiate_fixtures(*fixture_table_names) if fixture_table_names
384
+ def setup_with_fixtures
385
+ # Load fixtures once and begin transaction.
386
+ if use_transactional_fixtures
387
+ load_fixtures unless @already_loaded_fixtures
388
+ @already_loaded_fixtures = true
389
+ ActiveRecord::Base.lock_mutex
390
+ ActiveRecord::Base.connection.begin_db_transaction
391
+
392
+ # Load fixtures for every test.
393
+ else
394
+ load_fixtures
395
+ end
396
+
397
+ # Instantiate fixtures for every test if requested.
398
+ instantiate_fixtures if use_instantiated_fixtures
399
+ end
400
+
401
+ alias_method :setup, :setup_with_fixtures
402
+
403
+ def teardown_with_fixtures
404
+ # Rollback changes.
405
+ if use_transactional_fixtures
406
+ ActiveRecord::Base.connection.rollback_db_transaction
407
+ ActiveRecord::Base.unlock_mutex
408
+ end
352
409
  end
353
410
 
354
- def self.method_added(method_symbol)
355
- if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
356
- alias_method :setup_without_fixtures, :setup
357
- define_method(:setup) do
358
- instantiate_fixtures(*fixture_table_names) if fixture_table_names
359
- setup_without_fixtures
411
+ alias_method :teardown, :teardown_with_fixtures
412
+
413
+ def self.method_added(method)
414
+ case method.to_s
415
+ when 'setup'
416
+ unless method_defined?(:setup_without_fixtures)
417
+ alias_method :setup_without_fixtures, :setup
418
+ define_method(:setup) do
419
+ setup_with_fixtures
420
+ setup_without_fixtures
421
+ end
422
+ end
423
+ when 'teardown'
424
+ unless method_defined?(:teardown_without_fixtures)
425
+ alias_method :teardown_without_fixtures, :teardown
426
+ define_method(:teardown) do
427
+ teardown_without_fixtures
428
+ teardown_with_fixtures
429
+ end
360
430
  end
361
431
  end
362
432
  end
363
433
 
364
434
  private
365
- def instantiate_fixtures(*table_names)
366
- Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
435
+ def load_fixtures
436
+ @loaded_fixtures = {}
437
+ fixture_table_names.each do |table_name|
438
+ @loaded_fixtures[table_name] = Fixtures.create_fixtures(fixture_path, table_name)
439
+ end
367
440
  end
368
441
 
369
- def fixture_table_names
370
- self.class.read_inheritable_attribute("fixture_table_names")
442
+ def instantiate_fixtures
443
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
444
+ @loaded_fixtures.each do |table_name, fixtures|
445
+ Fixtures.instantiate_fixtures(self, table_name, fixtures)
446
+ end
371
447
  end
372
448
  end
449
+
373
450
  end
374
451
  end
@@ -0,0 +1,94 @@
1
+ module ActiveRecord
2
+ class IrreversibleMigration < ActiveRecordError
3
+ end
4
+
5
+ class Migration
6
+ class << self
7
+ def up() end
8
+ def down() end
9
+
10
+ private
11
+ def method_missing(method, *arguments, &block)
12
+ ActiveRecord::Base.connection.send(method, *arguments, &block)
13
+ end
14
+ end
15
+ end
16
+
17
+ class Migrator
18
+ class << self
19
+ def up(migrations_path, target_version = nil)
20
+ new(:up, migrations_path, target_version).migrate
21
+ end
22
+
23
+ def down(migrations_path, target_version = nil)
24
+ new(:down, migrations_path, target_version).migrate
25
+ end
26
+
27
+ def current_version
28
+ Base.connection.select_one("SELECT version FROM schema_info")["version"].to_i
29
+ end
30
+ end
31
+
32
+ def initialize(direction, migrations_path, target_version = nil)
33
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
34
+ Base.connection.initialize_schema_information
35
+ end
36
+
37
+ def current_version
38
+ self.class.current_version
39
+ end
40
+
41
+ def migrate
42
+ migration_classes do |version, migration_class|
43
+ Base.logger.info("Reached target version: #{@target_version}") and break if reached_target_version?(version)
44
+ next if irrelevant_migration?(version)
45
+
46
+ Base.logger.info "Migrating to #{migration_class} (#{version})"
47
+ migration_class.send(@direction)
48
+ set_schema_version(version)
49
+ end
50
+ end
51
+
52
+ private
53
+ def migration_classes
54
+ for migration_file in migration_files
55
+ load(migration_file)
56
+ version, name = migration_version_and_name(migration_file)
57
+ yield version, migration_class(name)
58
+ end
59
+ end
60
+
61
+ def migration_files
62
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
63
+ down? ? files.reverse : files
64
+ end
65
+
66
+ def migration_class(migration_name)
67
+ migration_name.camelize.constantize
68
+ end
69
+
70
+ def migration_version_and_name(migration_file)
71
+ return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
72
+ end
73
+
74
+ def set_schema_version(version)
75
+ Base.connection.update("UPDATE schema_info SET version = #{down? ? version.to_i - 1 : version.to_i}")
76
+ end
77
+
78
+ def up?
79
+ @direction == :up
80
+ end
81
+
82
+ def down?
83
+ @direction == :down
84
+ end
85
+
86
+ def reached_target_version?(version)
87
+ (up? && version.to_i - 1 == @target_version) || (down? && version.to_i == @target_version)
88
+ end
89
+
90
+ def irrelevant_migration?(version)
91
+ (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
92
+ end
93
+ end
94
+ end
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
 
11
11
  def composed_of_with_reflection(part_id, options = {})
12
12
  composed_of_without_reflection(part_id, options)
13
- write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
13
+ write_inheritable_array "aggregations", [ AggregateReflection.new(:composed_of, part_id, options, self) ]
14
14
  end
15
15
 
16
16
  alias_method :composed_of, :composed_of_with_reflection
@@ -24,7 +24,7 @@ module ActiveRecord
24
24
 
25
25
  def #{association_type}_with_reflection(association_id, options = {})
26
26
  #{association_type}_without_reflection(association_id, options)
27
- write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
27
+ write_inheritable_array "associations", [ AssociationReflection.new(:#{association_type}, association_id, options, self) ]
28
28
  end
29
29
 
30
30
  alias_method :#{association_type}, :#{association_type}_with_reflection
@@ -67,8 +67,8 @@ module ActiveRecord
67
67
  # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
68
68
  class MacroReflection
69
69
  attr_reader :active_record
70
- def initialize(name, options, active_record)
71
- @name, @options, @active_record = name, options, active_record
70
+ def initialize(macro, name, options, active_record)
71
+ @macro, @name, @options, @active_record = macro, name, options, active_record
72
72
  end
73
73
 
74
74
  # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
@@ -77,6 +77,12 @@ module ActiveRecord
77
77
  @name
78
78
  end
79
79
 
80
+ # Returns the name of the macro, so it would return :composed_of for
81
+ # "composed_of :balance, :class_name => 'Money'" or :has_many for "has_many :clients".
82
+ def macro
83
+ @macro
84
+ end
85
+
80
86
  # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
81
87
  # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
82
88
  def options
@@ -16,6 +16,7 @@ module ActiveRecord
16
16
  :too_short => "is too short (min is %d characters)",
17
17
  :wrong_length => "is the wrong length (should be %d characters)",
18
18
  :taken => "has already been taken",
19
+ :not_a_number => "is not a number",
19
20
  }
20
21
 
21
22
  # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
@@ -193,6 +194,20 @@ module ActiveRecord
193
194
  # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
194
195
  # these over the low-level calls to validate and validate_on_create when possible.
195
196
  module ClassMethods
197
+ DEFAULT_VALIDATION_OPTIONS = {
198
+ :on => :save,
199
+ :allow_nil => false,
200
+ :message => nil
201
+ }.freeze
202
+
203
+ DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge(
204
+ :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
205
+ :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
206
+ :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
207
+ ).freeze
208
+
209
+ ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
210
+
196
211
  def validate(*methods, &block)
197
212
  methods << block if block_given?
198
213
  write_inheritable_set(:validate, methods)
@@ -208,6 +223,31 @@ module ActiveRecord
208
223
  write_inheritable_set(:validate_on_update, methods)
209
224
  end
210
225
 
226
+ # Validates each attribute against a block.
227
+ #
228
+ # class Person < ActiveRecord::Base
229
+ # validates_each :first_name, :last_name do |record, attr|
230
+ # record.errors.add attr, 'starts with z.' if attr[0] == ?z
231
+ # end
232
+ # end
233
+ #
234
+ # Options:
235
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
236
+ # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
237
+ def validates_each(*attrs)
238
+ options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
239
+ attrs = attrs.flatten
240
+
241
+ # Declare the validation.
242
+ send(validation_method(options[:on] || :save)) do |record|
243
+ attrs.each do |attr|
244
+ value = record.send(attr)
245
+ next if value.nil? && options[:allow_nil]
246
+ yield record, attr, value
247
+ end
248
+ end
249
+ end
250
+
211
251
  # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
212
252
  #
213
253
  # Model:
@@ -279,48 +319,6 @@ module ActiveRecord
279
319
  end
280
320
  end
281
321
 
282
-
283
- DEFAULT_VALIDATION_OPTIONS = {
284
- :on => :save,
285
- :allow_nil => false,
286
- :message => nil
287
- }.freeze
288
-
289
- DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge(
290
- :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
291
- :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
292
- :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
293
- ).freeze
294
-
295
- ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
296
-
297
-
298
- # Validates each attribute against a block.
299
- #
300
- # class Person < ActiveRecord::Base
301
- # validates_each :first_name, :last_name do |record, attr|
302
- # record.errors.add attr, 'starts with z.' if attr[0] == ?z
303
- # end
304
- # end
305
- #
306
- # Options:
307
- # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
308
- # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
309
- def validates_each(*attrs)
310
- options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
311
- attrs = attrs.flatten
312
-
313
- # Declare the validation.
314
- send(validation_method(options[:on] || :save)) do |record|
315
- attrs.each do |attr|
316
- value = record.send(attr)
317
- next if value.nil? && options[:allow_nil]
318
- yield record, attr, value
319
- end
320
- end
321
- end
322
-
323
-
324
322
  # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
325
323
  #
326
324
  # class Person < ActiveRecord::Base
@@ -516,6 +514,42 @@ module ActiveRecord
516
514
  end
517
515
  end
518
516
 
517
+ # Validates whether the value of the specified attribute is numeric by trying to convert it to
518
+ # a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
519
+ # <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
520
+ #
521
+ # class Person < ActiveRecord::Base
522
+ # validates_numericality_of :value, :on => :create
523
+ # end
524
+ #
525
+ # Configuration options:
526
+ # * <tt>message</tt> - A custom error message (default is: "is not a number")
527
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
528
+ # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
529
+ def validates_numericality_of(*attr_names)
530
+ configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
531
+ :integer => false }
532
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
533
+
534
+ for attr_name in attr_names
535
+ if configuration[:only_integer]
536
+ # we have to use a regexp here, because Kernel.Integer accepts nil and "0xdeadbeef", but does not
537
+ # accept "099" and String#to_i accepts everything. The string containing the regexp is evaluated twice
538
+ # so we have to escape everything properly
539
+ class_eval(%(#{validation_method(configuration[:on])} %{
540
+ errors.add("#{attr_name}", "#{configuration[:message]}") unless #{attr_name}_before_type_cast.to_s =~ /^[\\\\+\\\\-]?\\\\d+$/
541
+ }))
542
+ else
543
+ class_eval(%(#{validation_method(configuration[:on])} %{
544
+ begin
545
+ Kernel.Float(#{attr_name}_before_type_cast)
546
+ rescue ArgumentError, TypeError
547
+ errors.add("#{attr_name}", "#{configuration[:message]}")
548
+ end
549
+ }))
550
+ end
551
+ end
552
+ end
519
553
 
520
554
  private
521
555
  def write_inheritable_set(key, methods)