activerecord 1.13.2 → 1.14.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 (144) hide show
  1. data/CHANGELOG +452 -10
  2. data/RUNNING_UNIT_TESTS +1 -1
  3. data/lib/active_record.rb +5 -2
  4. data/lib/active_record/acts/list.rb +1 -1
  5. data/lib/active_record/acts/tree.rb +29 -25
  6. data/lib/active_record/aggregations.rb +3 -2
  7. data/lib/active_record/associations.rb +783 -337
  8. data/lib/active_record/associations/association_collection.rb +7 -12
  9. data/lib/active_record/associations/association_proxy.rb +62 -24
  10. data/lib/active_record/associations/belongs_to_association.rb +27 -46
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
  13. data/lib/active_record/associations/has_many_association.rb +61 -56
  14. data/lib/active_record/associations/has_many_through_association.rb +144 -0
  15. data/lib/active_record/associations/has_one_association.rb +22 -16
  16. data/lib/active_record/base.rb +482 -182
  17. data/lib/active_record/calculations.rb +225 -0
  18. data/lib/active_record/callbacks.rb +7 -7
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
  20. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
  24. data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
  25. data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
  26. data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
  27. data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
  30. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
  31. data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
  32. data/lib/active_record/fixtures.rb +42 -17
  33. data/lib/active_record/locking.rb +36 -15
  34. data/lib/active_record/migration.rb +111 -8
  35. data/lib/active_record/observer.rb +25 -1
  36. data/lib/active_record/reflection.rb +103 -41
  37. data/lib/active_record/schema.rb +2 -2
  38. data/lib/active_record/schema_dumper.rb +55 -18
  39. data/lib/active_record/timestamp.rb +6 -6
  40. data/lib/active_record/validations.rb +65 -40
  41. data/lib/active_record/vendor/db2.rb +10 -5
  42. data/lib/active_record/vendor/simple.rb +693 -702
  43. data/lib/active_record/version.rb +2 -2
  44. data/rakefile +4 -4
  45. data/test/aaa_create_tables_test.rb +25 -6
  46. data/test/abstract_unit.rb +39 -1
  47. data/test/adapter_test.rb +31 -4
  48. data/test/associations_cascaded_eager_loading_test.rb +106 -0
  49. data/test/associations_go_eager_test.rb +85 -16
  50. data/test/associations_join_model_test.rb +338 -0
  51. data/test/associations_test.rb +129 -50
  52. data/test/base_test.rb +204 -49
  53. data/test/binary_test.rb +1 -1
  54. data/test/calculations_test.rb +169 -0
  55. data/test/callbacks_test.rb +5 -23
  56. data/test/class_inheritable_attributes_test.rb +1 -1
  57. data/test/column_alias_test.rb +1 -1
  58. data/test/connections/native_mysql/connection.rb +1 -0
  59. data/test/connections/native_openbase/connection.rb +22 -0
  60. data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
  61. data/test/connections/native_sqlite/connection.rb +1 -1
  62. data/test/connections/native_sqlite3/connection.rb +1 -0
  63. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
  64. data/test/connections/native_sybase/connection.rb +24 -0
  65. data/test/defaults_test.rb +18 -0
  66. data/test/deprecated_associations_test.rb +2 -2
  67. data/test/deprecated_finder_test.rb +0 -6
  68. data/test/finder_test.rb +26 -23
  69. data/test/fixtures/accounts.yml +10 -0
  70. data/test/fixtures/author.rb +31 -6
  71. data/test/fixtures/author_favorites.yml +4 -0
  72. data/test/fixtures/categories/special_categories.yml +9 -0
  73. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  74. data/test/fixtures/categories_posts.yml +4 -0
  75. data/test/fixtures/categorization.rb +5 -0
  76. data/test/fixtures/categorizations.yml +11 -0
  77. data/test/fixtures/category.rb +6 -0
  78. data/test/fixtures/company.rb +17 -5
  79. data/test/fixtures/company_in_module.rb +19 -5
  80. data/test/fixtures/db_definitions/db2.drop.sql +3 -0
  81. data/test/fixtures/db_definitions/db2.sql +121 -100
  82. data/test/fixtures/db_definitions/db22.sql +2 -2
  83. data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
  84. data/test/fixtures/db_definitions/firebird.sql +26 -0
  85. data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
  86. data/test/fixtures/db_definitions/mysql.sql +21 -1
  87. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  88. data/test/fixtures/db_definitions/openbase.sql +282 -0
  89. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  90. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  91. data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
  92. data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
  93. data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
  94. data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
  95. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  96. data/test/fixtures/db_definitions/postgresql.sql +22 -1
  97. data/test/fixtures/db_definitions/schema.rb +32 -0
  98. data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
  99. data/test/fixtures/db_definitions/sqlite.sql +18 -0
  100. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  101. data/test/fixtures/db_definitions/sqlserver.sql +23 -3
  102. data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
  103. data/test/fixtures/db_definitions/sybase.sql +204 -0
  104. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  106. data/test/fixtures/developers.yml +6 -1
  107. data/test/fixtures/developers_projects.yml +4 -0
  108. data/test/fixtures/funny_jokes.yml +14 -0
  109. data/test/fixtures/joke.rb +6 -0
  110. data/test/fixtures/legacy_thing.rb +3 -0
  111. data/test/fixtures/legacy_things.yml +3 -0
  112. data/test/fixtures/mixin.rb +1 -1
  113. data/test/fixtures/person.rb +4 -1
  114. data/test/fixtures/post.rb +26 -1
  115. data/test/fixtures/project.rb +1 -0
  116. data/test/fixtures/reader.rb +4 -0
  117. data/test/fixtures/readers.yml +4 -0
  118. data/test/fixtures/reply.rb +2 -1
  119. data/test/fixtures/tag.rb +5 -0
  120. data/test/fixtures/tagging.rb +6 -0
  121. data/test/fixtures/taggings.yml +18 -0
  122. data/test/fixtures/tags.yml +7 -0
  123. data/test/fixtures/tasks.yml +2 -2
  124. data/test/fixtures/topic.rb +2 -2
  125. data/test/fixtures/topics.yml +1 -0
  126. data/test/fixtures_test.rb +47 -13
  127. data/test/inheritance_test.rb +2 -2
  128. data/test/locking_test.rb +15 -1
  129. data/test/method_scoping_test.rb +248 -13
  130. data/test/migration_test.rb +68 -11
  131. data/test/mixin_nested_set_test.rb +1 -1
  132. data/test/modules_test.rb +6 -1
  133. data/test/readonly_test.rb +1 -1
  134. data/test/reflection_test.rb +63 -9
  135. data/test/schema_dumper_test.rb +41 -0
  136. data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
  137. data/test/threaded_connections_test.rb +10 -0
  138. data/test/unconnected_test.rb +12 -5
  139. data/test/validations_test.rb +197 -10
  140. metadata +295 -260
  141. data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
  142. data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
  143. data/test/fixtures/fixture_database.sqlite +0 -0
  144. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -21,6 +21,8 @@ module ActiveRecord #:nodoc:
21
21
  end
22
22
  class RecordNotFound < ActiveRecordError #:nodoc:
23
23
  end
24
+ class RecordNotSaved < ActiveRecordError #:nodoc:
25
+ end
24
26
  class StatementInvalid < ActiveRecordError #:nodoc:
25
27
  end
26
28
  class PreparedStatementInvalid < ActiveRecordError #:nodoc:
@@ -242,22 +244,16 @@ module ActiveRecord #:nodoc:
242
244
  # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
243
245
  # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
244
246
  cattr_accessor :logger
245
-
247
+
248
+ include Reloadable::Subclasses
249
+
246
250
  def self.inherited(child) #:nodoc:
247
251
  @@subclasses[self] ||= []
248
252
  @@subclasses[self] << child
249
253
  super
250
254
  end
251
255
 
252
- # Allow all subclasses of AR::Base to be reloaded in dev mode, unless they
253
- # explicitly decline the honor. USE WITH CAUTION. Only AR subclasses kept
254
- # in the framework should use the flag, so #reset_subclasses and so forth
255
- # leave it alone.
256
- def self.reloadable? #:nodoc:
257
- true
258
- end
259
-
260
- def self.reset_subclasses
256
+ def self.reset_subclasses #:nodoc:
261
257
  nonreloadables = []
262
258
  subclasses.each do |klass|
263
259
  unless klass.reloadable?
@@ -310,12 +306,12 @@ module ActiveRecord #:nodoc:
310
306
  # This is set to :local by default.
311
307
  cattr_accessor :default_timezone
312
308
  @@default_timezone = :local
313
-
309
+
314
310
  # Determines whether or not to use a connection for each thread, or a single shared connection for all threads.
315
- # Defaults to true; Railties' WEBrick server sets this to false.
311
+ # Defaults to false. Set to true if you're writing a threaded application.
316
312
  cattr_accessor :allow_concurrency
317
- @@allow_concurrency = true
318
-
313
+ @@allow_concurrency = false
314
+
319
315
  # Determines whether to speed up access by generating optimized reader
320
316
  # methods to avoid expensive calls to method_missing when accessing
321
317
  # attributes by name. You might want to set this to false in development
@@ -330,7 +326,7 @@ module ActiveRecord #:nodoc:
330
326
  # supports migrations. Use :ruby if you want to have different database
331
327
  # adapters for, e.g., your development and test environments.
332
328
  cattr_accessor :schema_format
333
- @@schema_format = :sql
329
+ @@schema_format = :ruby
334
330
 
335
331
  class << self # Class methods
336
332
  # Find operates with three different retrieval approaches:
@@ -377,53 +373,16 @@ module ActiveRecord #:nodoc:
377
373
  # Person.find(:all, :group => "category")
378
374
  def find(*args)
379
375
  options = extract_options_from_args!(args)
380
-
381
- # Inherit :readonly from finder scope if set. Otherwise,
382
- # if :joins is not blank then :readonly defaults to true.
383
- unless options.has_key?(:readonly)
384
- if scoped?(:find, :readonly)
385
- options[:readonly] = scope(:find, :readonly)
386
- elsif !options[:joins].blank?
387
- options[:readonly] = true
388
- end
389
- end
376
+ validate_find_options(options)
377
+ set_readonly_option!(options)
390
378
 
391
379
  case args.first
392
- when :first
393
- find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first
394
- when :all
395
- records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options))
396
- records.each { |record| record.readonly! } if options[:readonly]
397
- records
398
- else
399
- return args.first if args.first.kind_of?(Array) && args.first.empty?
400
- expects_array = args.first.kind_of?(Array)
401
-
402
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
403
-
404
- ids = args.flatten.compact.uniq
405
- case ids.size
406
- when 0
407
- raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
408
- when 1
409
- if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" }))
410
- return expects_array ? [ result ] : result
411
- else
412
- raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
413
- end
414
- else
415
- # Find multiple ids
416
- ids_list = ids.map { |id| sanitize(id) }.join(',')
417
- result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"}))
418
- if result.size == ids.size
419
- return result
420
- else
421
- raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
422
- end
423
- end
380
+ when :first then find_initial(options)
381
+ when :all then find_every(options)
382
+ else find_from_ids(args, options)
424
383
  end
425
384
  end
426
-
385
+
427
386
  # Works like find(:all), but requires a complete SQL string. Examples:
428
387
  # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
429
388
  # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
@@ -444,9 +403,8 @@ module ActiveRecord #:nodoc:
444
403
  if attributes.is_a?(Array)
445
404
  attributes.collect { |attr| create(attr) }
446
405
  else
447
- attributes.reverse_merge!(scope(:create)) if scoped?(:create)
448
-
449
406
  object = new(attributes)
407
+ scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create)
450
408
  object.save
451
409
  object
452
410
  end
@@ -454,6 +412,16 @@ module ActiveRecord #:nodoc:
454
412
 
455
413
  # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
456
414
  # and returns it. If the save fails under validations, the unsaved object is still returned.
415
+ #
416
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
417
+ # +attributes+ and an array of objects is returned.
418
+ #
419
+ # Example of updating one record:
420
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
421
+ #
422
+ # Example of updating multiple records:
423
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
424
+ # Person.update(people.keys, people.values)
457
425
  def update(id, attributes)
458
426
  if id.is_a?(Array)
459
427
  idx = -1
@@ -482,7 +450,7 @@ module ActiveRecord #:nodoc:
482
450
  # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
483
451
  def update_all(updates, conditions = nil)
484
452
  sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} "
485
- add_conditions!(sql, conditions)
453
+ add_conditions!(sql, conditions, scope(:find))
486
454
  connection.update(sql, "#{name} Update")
487
455
  end
488
456
 
@@ -498,19 +466,10 @@ module ActiveRecord #:nodoc:
498
466
  # Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
499
467
  def delete_all(conditions = nil)
500
468
  sql = "DELETE FROM #{table_name} "
501
- add_conditions!(sql, conditions)
469
+ add_conditions!(sql, conditions, scope(:find))
502
470
  connection.delete(sql, "#{name} Delete all")
503
471
  end
504
472
 
505
- # Returns the number of records that meet the +conditions+. Zero is returned if no records match. Example:
506
- # Product.count "sales > 1"
507
- def count(conditions = nil, joins = nil)
508
- sql = "SELECT COUNT(*) FROM #{table_name} "
509
- sql << " #{joins} " if joins
510
- add_conditions!(sql, conditions)
511
- count_by_sql(sql)
512
- end
513
-
514
473
  # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
515
474
  # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
516
475
  def count_by_sql(sql)
@@ -532,6 +491,7 @@ module ActiveRecord #:nodoc:
532
491
  update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote(id)}"
533
492
  end
534
493
 
494
+
535
495
  # Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
536
496
  # <tt>attributes=(attributes)</tt>. Their assignment will simply be ignored. Instead, you can use the direct writer
537
497
  # methods to do assignment. This is meant to protect sensitive attributes from being overwritten by URL/form hackers. Example:
@@ -569,6 +529,7 @@ module ActiveRecord #:nodoc:
569
529
  read_inheritable_attribute("attr_accessible")
570
530
  end
571
531
 
532
+
572
533
  # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
573
534
  # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
574
535
  # object must be of that class on retrieval or +SerializationTypeMismatch+ will be raised.
@@ -581,6 +542,7 @@ module ActiveRecord #:nodoc:
581
542
  read_inheritable_attribute("attr_serialized") or write_inheritable_attribute("attr_serialized", {})
582
543
  end
583
544
 
545
+
584
546
  # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
585
547
  # directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
586
548
  # to guess the table name from even when called on Reply. The rules used to do the guess are handled by the Inflector class
@@ -599,9 +561,9 @@ module ActiveRecord #:nodoc:
599
561
  reset_table_name
600
562
  end
601
563
 
602
- def reset_table_name
603
- name = "#{table_name_prefix}#{undecorated_table_name(class_name_of_active_record_descendant(self))}#{table_name_suffix}"
604
- set_table_name name
564
+ def reset_table_name #:nodoc:
565
+ name = "#{table_name_prefix}#{undecorated_table_name(base_class.name)}#{table_name_suffix}"
566
+ set_table_name(name)
605
567
  name
606
568
  end
607
569
 
@@ -611,13 +573,13 @@ module ActiveRecord #:nodoc:
611
573
  reset_primary_key
612
574
  end
613
575
 
614
- def reset_primary_key
576
+ def reset_primary_key #:nodoc:
615
577
  key = 'id'
616
578
  case primary_key_prefix_type
617
579
  when :table_name
618
- key = Inflector.foreign_key(class_name_of_active_record_descendant(self), false)
580
+ key = Inflector.foreign_key(base_class.name, false)
619
581
  when :table_name_with_underscore
620
- key = Inflector.foreign_key(class_name_of_active_record_descendant(self))
582
+ key = Inflector.foreign_key(base_class.name)
621
583
  end
622
584
  set_primary_key(key)
623
585
  key
@@ -630,11 +592,11 @@ module ActiveRecord #:nodoc:
630
592
 
631
593
  # Lazy-set the sequence name to the connection's default. This method
632
594
  # is only ever called once since set_sequence_name overrides it.
633
- def sequence_name
595
+ def sequence_name #:nodoc:
634
596
  reset_sequence_name
635
597
  end
636
598
 
637
- def reset_sequence_name
599
+ def reset_sequence_name #:nodoc:
638
600
  default = connection.default_sequence_name(table_name, primary_key)
639
601
  set_sequence_name(default)
640
602
  default
@@ -648,7 +610,7 @@ module ActiveRecord #:nodoc:
648
610
  # class Project < ActiveRecord::Base
649
611
  # set_table_name "project"
650
612
  # end
651
- def set_table_name( value=nil, &block )
613
+ def set_table_name(value = nil, &block)
652
614
  define_attr_method :table_name, value, &block
653
615
  end
654
616
  alias :table_name= :set_table_name
@@ -662,7 +624,7 @@ module ActiveRecord #:nodoc:
662
624
  # class Project < ActiveRecord::Base
663
625
  # set_primary_key "sysid"
664
626
  # end
665
- def set_primary_key( value=nil, &block )
627
+ def set_primary_key(value = nil, &block)
666
628
  define_attr_method :primary_key, value, &block
667
629
  end
668
630
  alias :primary_key= :set_primary_key
@@ -678,7 +640,7 @@ module ActiveRecord #:nodoc:
678
640
  # original_inheritance_column + "_id"
679
641
  # end
680
642
  # end
681
- def set_inheritance_column( value=nil, &block )
643
+ def set_inheritance_column(value = nil, &block)
682
644
  define_attr_method :inheritance_column, value, &block
683
645
  end
684
646
  alias :inheritance_column= :set_inheritance_column
@@ -699,7 +661,7 @@ module ActiveRecord #:nodoc:
699
661
  # class Project < ActiveRecord::Base
700
662
  # set_sequence_name "projectseq" # default would have been "project_seq"
701
663
  # end
702
- def set_sequence_name( value=nil, &block )
664
+ def set_sequence_name(value = nil, &block)
703
665
  define_attr_method :sequence_name, value, &block
704
666
  end
705
667
  alias :sequence_name= :set_sequence_name
@@ -742,6 +704,7 @@ module ActiveRecord #:nodoc:
742
704
  @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
743
705
  end
744
706
 
707
+ # Returns an array of column names as strings.
745
708
  def column_names
746
709
  @column_names ||= columns.map { |column| column.name }
747
710
  end
@@ -755,7 +718,7 @@ module ActiveRecord #:nodoc:
755
718
  # 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
756
719
  # 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
757
720
  # is available.
758
- def column_methods_hash
721
+ def column_methods_hash #:nodoc:
759
722
  @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
760
723
  attr_name = attr.to_s
761
724
  methods[attr.to_sym] = attr_name
@@ -767,7 +730,7 @@ module ActiveRecord #:nodoc:
767
730
  end
768
731
 
769
732
  # Contains the names of the generated reader methods.
770
- def read_methods
733
+ def read_methods #:nodoc:
771
734
  @read_methods ||= Set.new
772
735
  end
773
736
 
@@ -834,35 +797,88 @@ module ActiveRecord #:nodoc:
834
797
  end
835
798
 
836
799
  # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
837
- # method_name may be :find or :create.
838
- # :find parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
839
- # <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options.
840
- # :create parameters are an attributes hash.
800
+ # method_name may be :find or :create. :find parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
801
+ # <tt>:include</tt>, <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options. :create parameters are an attributes hash.
841
802
  #
842
803
  # Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do
843
804
  # Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
844
805
  # a = Article.create(1)
845
- # a.blog_id == 1
806
+ # a.blog_id # => 1
846
807
  # end
847
- def with_scope(method_scoping = {})
808
+ #
809
+ # In nested scopings, all previous parameters are overwritten by inner rule
810
+ # except :conditions in :find, that are merged as hash.
811
+ #
812
+ # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do
813
+ # Article.with_scope(:find => { :limit => 10})
814
+ # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
815
+ # end
816
+ # Article.with_scope(:find => { :conditions => "author_id = 3" })
817
+ # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
818
+ # end
819
+ # end
820
+ #
821
+ # You can ignore any previous scopings by using <tt>with_exclusive_scope</tt> method.
822
+ #
823
+ # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do
824
+ # Article.with_exclusive_scope(:find => { :limit => 10 })
825
+ # Article.find(:all) # => SELECT * from articles LIMIT 10
826
+ # end
827
+ # end
828
+ def with_scope(method_scoping = {}, action = :merge, &block)
829
+ method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
830
+
848
831
  # Dup first and second level of hash (method and params).
849
832
  method_scoping = method_scoping.inject({}) do |hash, (method, params)|
850
- hash[method] = params.dup
833
+ hash[method] = (params == true) ? params : params.dup
851
834
  hash
852
835
  end
853
836
 
854
- method_scoping.assert_valid_keys [:find, :create]
837
+ method_scoping.assert_valid_keys([ :find, :create ])
838
+
855
839
  if f = method_scoping[:find]
856
- f.assert_valid_keys [:conditions, :joins, :offset, :limit, :readonly]
840
+ f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :readonly ])
857
841
  f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
858
842
  end
859
843
 
860
- raise ArgumentError, "Nested scopes are not yet supported: #{scoped_methods.inspect}" unless scoped_methods.nil?
844
+ # Merge scopings
845
+ if action == :merge && current_scoped_methods
846
+ method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
847
+ case hash[method]
848
+ when Hash
849
+ if method == :find
850
+ (hash[method].keys + params.keys).uniq.each do |key|
851
+ merge = hash[method][key] && params[key] # merge if both scopes have the same key
852
+ if key == :conditions && merge
853
+ hash[method][key] = [params[key], hash[method][key]].collect{|sql| "( %s )" % sanitize_sql(sql)}.join(" AND ")
854
+ elsif key == :include && merge
855
+ hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
856
+ else
857
+ hash[method][key] = hash[method][key] || params[key]
858
+ end
859
+ end
860
+ else
861
+ hash[method] = params.merge(hash[method])
862
+ end
863
+ else
864
+ hash[method] = params
865
+ end
866
+ hash
867
+ end
868
+ end
869
+
870
+ self.scoped_methods << method_scoping
861
871
 
862
- self.scoped_methods = method_scoping
863
- yield
864
- ensure
865
- self.scoped_methods = nil
872
+ begin
873
+ yield
874
+ ensure
875
+ self.scoped_methods.pop
876
+ end
877
+ end
878
+
879
+ # Works like with_scope, but discards any nested properties.
880
+ def with_exclusive_scope(method_scoping = {}, &block)
881
+ with_scope(method_scoping, :overwrite, &block)
866
882
  end
867
883
 
868
884
  # Overwrite the default class equality method to provide support for association proxies.
@@ -871,17 +887,89 @@ module ActiveRecord #:nodoc:
871
887
  end
872
888
 
873
889
  # Deprecated
874
- def threaded_connections
890
+ def threaded_connections #:nodoc:
875
891
  allow_concurrency
876
892
  end
877
893
 
878
894
  # Deprecated
879
- def threaded_connections=(value)
895
+ def threaded_connections=(value) #:nodoc:
880
896
  self.allow_concurrency = value
881
897
  end
882
898
 
883
-
899
+ # Returns the base AR subclass that this class descends from. If A
900
+ # extends AR::Base, A.base_class will return A. If B descends from A
901
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
902
+ def base_class
903
+ class_of_active_record_descendant(self)
904
+ end
905
+
906
+ # Set this to true if this is an abstract class (see #abstract_class?).
907
+ attr_accessor :abstract_class
908
+
909
+ # Returns whether this class is a base AR class. If A is a base class and
910
+ # B descends from A, then B.base_class will return B.
911
+ def abstract_class?
912
+ abstract_class == true
913
+ end
914
+
884
915
  private
916
+ def find_initial(options)
917
+ options.update(:limit => 1) unless options[:include]
918
+ find_every(options).first
919
+ end
920
+
921
+ def find_every(options)
922
+ records = scoped?(:find, :include) || options[:include] ?
923
+ find_with_associations(options) :
924
+ find_by_sql(construct_finder_sql(options))
925
+
926
+ records.each { |record| record.readonly! } if options[:readonly]
927
+
928
+ records
929
+ end
930
+
931
+ def find_from_ids(ids, options)
932
+ expects_array = ids.first.kind_of?(Array)
933
+ return ids.first if expects_array && ids.first.empty?
934
+
935
+ ids = ids.flatten.compact.uniq
936
+
937
+ case ids.size
938
+ when 0
939
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
940
+ when 1
941
+ result = find_one(ids.first, options)
942
+ expects_array ? [ result ] : result
943
+ else
944
+ find_some(ids, options)
945
+ end
946
+ end
947
+
948
+ def find_one(id, options)
949
+ conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
950
+ options = options.merge :conditions => "#{table_name}.#{primary_key} = #{sanitize(id)}#{conditions}"
951
+
952
+ if result = find_initial(options)
953
+ result
954
+ else
955
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
956
+ end
957
+ end
958
+
959
+ def find_some(ids, options)
960
+ conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
961
+ ids_list = ids.map { |id| sanitize(id) }.join(',')
962
+ options = options.merge :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"
963
+
964
+ result = find_every(options)
965
+
966
+ if result.size == ids.size
967
+ result
968
+ else
969
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
970
+ end
971
+ end
972
+
885
973
  # Finder methods must instantiate through this method to work with the single-table inheritance model
886
974
  # that makes it possible to create objects of different types from the same table.
887
975
  def instantiate(record)
@@ -909,36 +997,62 @@ module ActiveRecord #:nodoc:
909
997
  object
910
998
  end
911
999
 
912
- # Returns the name of the type of the record using the current module as a prefix. So descendents of
913
- # MyApp::Business::Account would appear as "MyApp::Business::AccountSubclass".
1000
+ # Nest the type name in the same module as this class.
1001
+ # Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo
914
1002
  def type_name_with_module(type_name)
915
- self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
1003
+ "#{self.name.sub(/(::)?[^:]+$/, '')}#{$1}#{type_name}"
916
1004
  end
917
1005
 
918
1006
  def construct_finder_sql(options)
919
- sql = "SELECT #{options[:select] || '*'} FROM #{table_name} "
920
- add_joins!(sql, options)
921
- add_conditions!(sql, options[:conditions])
1007
+ scope = scope(:find)
1008
+ sql = "SELECT #{(scope && scope[:select]) || options[:select] || '*'} "
1009
+ sql << "FROM #{(scope && scope[:from]) || options[:from] || table_name} "
1010
+
1011
+ add_joins!(sql, options, scope)
1012
+ add_conditions!(sql, options[:conditions], scope)
1013
+
922
1014
  sql << " GROUP BY #{options[:group]} " if options[:group]
923
1015
  sql << " ORDER BY #{options[:order]} " if options[:order]
924
- add_limit!(sql, options)
1016
+
1017
+ add_limit!(sql, options, scope)
1018
+
925
1019
  sql
926
1020
  end
927
1021
 
928
- def add_limit!(sql, options)
929
- options[:limit] ||= scope(:find, :limit)
930
- options[:offset] ||= scope(:find, :offset)
1022
+ # Merges includes so that the result is a valid +include+
1023
+ def merge_includes(first, second)
1024
+ safe_to_array(first) + safe_to_array(second)
1025
+ end
1026
+
1027
+ # Object#to_a is deprecated, though it does have the desired behaviour
1028
+ def safe_to_array(o)
1029
+ case o
1030
+ when NilClass
1031
+ []
1032
+ when Array
1033
+ o
1034
+ else
1035
+ [o]
1036
+ end
1037
+ end
1038
+
1039
+ def add_limit!(sql, options, scope)
1040
+ if scope
1041
+ options[:limit] ||= scope[:limit]
1042
+ options[:offset] ||= scope[:offset]
1043
+ end
931
1044
  connection.add_limit_offset!(sql, options)
932
1045
  end
933
1046
 
934
- def add_joins!(sql, options)
935
- join = scope(:find, :joins) || options[:joins]
1047
+ def add_joins!(sql, options, scope)
1048
+ join = (scope && scope[:joins]) || options[:joins]
936
1049
  sql << " #{join} " if join
937
1050
  end
938
1051
 
939
1052
  # Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed.
940
- def add_conditions!(sql, conditions)
941
- segments = [scope(:find, :conditions)]
1053
+ def add_conditions!(sql, conditions, scope)
1054
+ segments = []
1055
+ segments << sanitize_sql(scope[:conditions]) if scope && scope[:conditions]
942
1056
  segments << sanitize_sql(conditions) unless conditions.nil?
943
1057
  segments << type_condition unless descends_from_active_record?
944
1058
  segments.compact!
@@ -955,7 +1069,7 @@ module ActiveRecord #:nodoc:
955
1069
  end
956
1070
 
957
1071
  # Guesses the table name, but does not decorate it with prefix and suffix information.
958
- def undecorated_table_name(class_name = class_name_of_active_record_descendant(self))
1072
+ def undecorated_table_name(class_name = base_class.name)
959
1073
  table_name = Inflector.underscore(Inflector.demodulize(class_name))
960
1074
  table_name = Inflector.pluralize(table_name) if pluralize_table_names
961
1075
  table_name
@@ -969,31 +1083,53 @@ module ActiveRecord #:nodoc:
969
1083
  # is actually find_all_by_amount(amount, options).
970
1084
  def method_missing(method_id, *arguments)
971
1085
  if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
972
- finder = determine_finder(match)
1086
+ finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
973
1087
 
974
1088
  attribute_names = extract_attribute_names_from_match(match)
975
1089
  super unless all_attributes_exists?(attribute_names)
976
1090
 
977
1091
  conditions = construct_conditions_from_arguments(attribute_names, arguments)
978
1092
 
979
- if arguments[attribute_names.length].is_a?(Hash)
980
- find(finder, { :conditions => conditions }.update(arguments[attribute_names.length]))
981
- else
982
- send("find_#{finder}", conditions, *arguments[attribute_names.length..-1]) # deprecated API
1093
+ case extra_options = arguments[attribute_names.size]
1094
+ when nil
1095
+ options = { :conditions => conditions }
1096
+ set_readonly_option!(options)
1097
+ send(finder, options)
1098
+
1099
+ when Hash
1100
+ finder_options = extra_options.merge(:conditions => conditions)
1101
+ validate_find_options(finder_options)
1102
+ set_readonly_option!(finder_options)
1103
+
1104
+ if extra_options[:conditions]
1105
+ with_scope(:find => { :conditions => extra_options[:conditions] }) do
1106
+ send(finder, finder_options)
1107
+ end
1108
+ else
1109
+ send(finder, finder_options)
1110
+ end
1111
+
1112
+ else
1113
+ send(deprecated_finder, conditions, *arguments[attribute_names.length..-1]) # deprecated API
983
1114
  end
984
1115
  elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
985
1116
  attribute_names = extract_attribute_names_from_match(match)
986
1117
  super unless all_attributes_exists?(attribute_names)
987
1118
 
988
- find(:first, :conditions => construct_conditions_from_arguments(attribute_names, arguments)) ||
989
- create(construct_attributes_from_arguments(attribute_names, arguments))
1119
+ options = { :conditions => construct_conditions_from_arguments(attribute_names, arguments) }
1120
+ set_readonly_option!(options)
1121
+ find_initial(options) || create(construct_attributes_from_arguments(attribute_names, arguments))
990
1122
  else
991
1123
  super
992
1124
  end
993
1125
  end
994
1126
 
995
1127
  def determine_finder(match)
996
- match.captures.first == 'all_by' ? :all : :first
1128
+ match.captures.first == 'all_by' ? :find_every : :find_initial
1129
+ end
1130
+
1131
+ def determine_deprecated_finder(match)
1132
+ match.captures.first == 'all_by' ? :find_all : :find_first
997
1133
  end
998
1134
 
999
1135
  def extract_attribute_names_from_match(match)
@@ -1055,60 +1191,74 @@ module ActiveRecord #:nodoc:
1055
1191
  end
1056
1192
 
1057
1193
  protected
1058
- def subclasses
1194
+ def subclasses #:nodoc:
1059
1195
  @@subclasses[self] ||= []
1060
1196
  @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
1061
1197
  end
1062
1198
 
1063
1199
  # Test whether the given method and optional key are scoped.
1064
- def scoped?(method, key = nil)
1065
- scoped_methods and scoped_methods.has_key?(method) and (key.nil? or scope(method).has_key?(key))
1200
+ def scoped?(method, key = nil) #:nodoc:
1201
+ if current_scoped_methods && (scope = current_scoped_methods[method])
1202
+ !key || scope.has_key?(key)
1203
+ end
1066
1204
  end
1067
1205
 
1068
1206
  # Retrieve the scope for the given method and optional key.
1069
- def scope(method, key = nil)
1070
- if scoped_methods and scope = scoped_methods[method]
1207
+ def scope(method, key = nil) #:nodoc:
1208
+ if current_scoped_methods && (scope = current_scoped_methods[method])
1071
1209
  key ? scope[key] : scope
1072
1210
  end
1073
1211
  end
1074
1212
 
1075
- def scoped_methods
1076
- if allow_concurrency
1077
- Thread.current[:scoped_methods] ||= {}
1078
- Thread.current[:scoped_methods][self] ||= nil
1079
- else
1080
- @scoped_methods ||= nil
1081
- end
1213
+ def thread_safe_scoped_methods #:nodoc:
1214
+ scoped_methods = (Thread.current[:scoped_methods] ||= {})
1215
+ scoped_methods[self] ||= []
1082
1216
  end
1083
-
1084
- def scoped_methods=(value)
1085
- if allow_concurrency
1086
- Thread.current[:scoped_methods] ||= {}
1087
- Thread.current[:scoped_methods][self] = value
1088
- else
1089
- @scoped_methods = value
1090
- end
1217
+
1218
+ def single_threaded_scoped_methods #:nodoc:
1219
+ @scoped_methods ||= []
1220
+ end
1221
+
1222
+ # pick up the correct scoped_methods version from @@allow_concurrency
1223
+ if @@allow_concurrency
1224
+ alias_method :scoped_methods, :thread_safe_scoped_methods
1225
+ else
1226
+ alias_method :scoped_methods, :single_threaded_scoped_methods
1227
+ end
1228
+
1229
+ def current_scoped_methods #:nodoc:
1230
+ scoped_methods.last
1091
1231
  end
1092
1232
 
1093
1233
  # Returns the class type of the record using the current module as a prefix. So descendents of
1094
1234
  # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
1095
1235
  def compute_type(type_name)
1096
- type_name_with_module(type_name).split("::").inject(Object) do |final_type, part|
1097
- final_type.const_get(part)
1236
+ modularized_name = type_name_with_module(type_name)
1237
+ begin
1238
+ instance_eval(modularized_name)
1239
+ rescue NameError => e
1240
+ first_module = modularized_name.split("::").first
1241
+ raise unless e.to_s.include? first_module
1242
+ instance_eval(type_name)
1098
1243
  end
1099
1244
  end
1100
1245
 
1101
- # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
1102
- def class_name_of_active_record_descendant(klass)
1103
- if klass.superclass == Base
1104
- klass.name
1246
+ # Returns the class descending directly from ActiveRecord in the inheritance hierarchy.
1247
+ def class_of_active_record_descendant(klass)
1248
+ if klass.superclass == Base || klass.superclass.abstract_class?
1249
+ klass
1105
1250
  elsif klass.superclass.nil?
1106
1251
  raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
1107
1252
  else
1108
- class_name_of_active_record_descendant(klass.superclass)
1253
+ class_of_active_record_descendant(klass.superclass)
1109
1254
  end
1110
1255
  end
1111
1256
 
1257
+ # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
1258
+ def class_name_of_active_record_descendant(klass) #:nodoc:
1259
+ klass.base_class.name
1260
+ end
1261
+
1112
1262
  # Accepts an array or string. The string is returned untouched, but the array has each value
1113
1263
  # sanitized and interpolated into the sql statement.
1114
1264
  # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
@@ -1127,14 +1277,13 @@ module ActiveRecord #:nodoc:
1127
1277
 
1128
1278
  alias_method :sanitize_conditions, :sanitize_sql
1129
1279
 
1130
- def replace_bind_variables(statement, values)
1280
+ def replace_bind_variables(statement, values) #:nodoc:
1131
1281
  raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
1132
1282
  bound = values.dup
1133
1283
  statement.gsub('?') { quote_bound_value(bound.shift) }
1134
1284
  end
1135
1285
 
1136
- def replace_named_bind_variables(statement, bind_vars)
1137
- raise_if_bind_arity_mismatch(statement, statement.scan(/:(\w+)/).uniq.size, bind_vars.size)
1286
+ def replace_named_bind_variables(statement, bind_vars) #:nodoc:
1138
1287
  statement.gsub(/:(\w+)/) do
1139
1288
  match = $1.to_sym
1140
1289
  if bind_vars.include?(match)
@@ -1145,7 +1294,7 @@ module ActiveRecord #:nodoc:
1145
1294
  end
1146
1295
  end
1147
1296
 
1148
- def quote_bound_value(value)
1297
+ def quote_bound_value(value) #:nodoc:
1149
1298
  if (value.respond_to?(:map) && !value.is_a?(String))
1150
1299
  value.map { |v| connection.quote(v) }.join(',')
1151
1300
  else
@@ -1153,23 +1302,36 @@ module ActiveRecord #:nodoc:
1153
1302
  end
1154
1303
  end
1155
1304
 
1156
- def raise_if_bind_arity_mismatch(statement, expected, provided)
1305
+ def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
1157
1306
  unless expected == provided
1158
1307
  raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
1159
1308
  end
1160
1309
  end
1161
1310
 
1162
- def extract_options_from_args!(args)
1163
- options = args.last.is_a?(Hash) ? args.pop : {}
1164
- validate_find_options(options)
1165
- options
1311
+ def extract_options_from_args!(args) #:nodoc:
1312
+ args.last.is_a?(Hash) ? args.pop : {}
1166
1313
  end
1167
1314
 
1168
- def validate_find_options(options)
1169
- options.assert_valid_keys [:conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group]
1315
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
1316
+ :order, :select, :readonly, :group, :from ]
1317
+
1318
+ def validate_find_options(options) #:nodoc:
1319
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
1320
+ end
1321
+
1322
+ def set_readonly_option!(options) #:nodoc:
1323
+ # Inherit :readonly from finder scope if set. Otherwise,
1324
+ # if :joins is not blank then :readonly defaults to true.
1325
+ unless options.has_key?(:readonly)
1326
+ if scoped?(:find, :readonly)
1327
+ options[:readonly] = scope(:find, :readonly)
1328
+ elsif !options[:joins].blank?
1329
+ options[:readonly] = true
1330
+ end
1331
+ end
1170
1332
  end
1171
1333
 
1172
- def encode_quoted_value(value)
1334
+ def encode_quoted_value(value) #:nodoc:
1173
1335
  quoted_value = connection.quote(value)
1174
1336
  quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
1175
1337
  quoted_value
@@ -1222,9 +1384,15 @@ module ActiveRecord #:nodoc:
1222
1384
  # * No record exists: Creates a new record with values matching those of the object attributes.
1223
1385
  # * A record does exist: Updates the record with values matching those of the object attributes.
1224
1386
  def save
1225
- raise ActiveRecord::ReadOnlyRecord if readonly?
1387
+ raise ReadOnlyRecord if readonly?
1226
1388
  create_or_update
1227
1389
  end
1390
+
1391
+ # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
1392
+ # RecordNotSaved exception
1393
+ def save!
1394
+ save || raise(RecordNotSaved)
1395
+ end
1228
1396
 
1229
1397
  # Deletes the record in the database and freezes this instance to reflect that no changes should
1230
1398
  # be made (since they can't be persisted).
@@ -1328,20 +1496,39 @@ module ActiveRecord #:nodoc:
1328
1496
  # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
1329
1497
  # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
1330
1498
  # attributes not included in that won't be allowed to be mass-assigned.
1331
- def attributes=(attributes)
1332
- return if attributes.nil?
1499
+ def attributes=(new_attributes)
1500
+ return if new_attributes.nil?
1501
+ attributes = new_attributes.dup
1333
1502
  attributes.stringify_keys!
1334
1503
 
1335
1504
  multi_parameter_attributes = []
1336
1505
  remove_attributes_protected_from_mass_assignment(attributes).each do |k, v|
1337
1506
  k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
1338
1507
  end
1508
+
1339
1509
  assign_multiparameter_attributes(multi_parameter_attributes)
1340
1510
  end
1341
1511
 
1512
+
1342
1513
  # Returns a hash of all the attributes with their names as keys and clones of their objects as values.
1343
- def attributes
1344
- clone_attributes :read_attribute
1514
+ def attributes(options = nil)
1515
+ attributes = clone_attributes :read_attribute
1516
+
1517
+ if options.nil?
1518
+ attributes
1519
+ else
1520
+ if except = options[:except]
1521
+ except = Array(except).collect { |attribute| attribute.to_s }
1522
+ except.each { |attribute_name| attributes.delete(attribute_name) }
1523
+ attributes
1524
+ elsif only = options[:only]
1525
+ only = Array(only).collect { |attribute| attribute.to_s }
1526
+ attributes.delete_if { |key, value| !only.include?(key) }
1527
+ attributes
1528
+ else
1529
+ raise ArgumentError, "Options does not specify :except or :only (#{options.keys.inspect})"
1530
+ end
1531
+ end
1345
1532
  end
1346
1533
 
1347
1534
  # Returns a hash of cloned attributes before typecasting and deserialization.
@@ -1418,14 +1605,108 @@ module ActiveRecord #:nodoc:
1418
1605
  @attributes.frozen?
1419
1606
  end
1420
1607
 
1608
+ # Records loaded through joins with piggy-back attributes will be marked as read only as they cannot be saved and return true to this query.
1421
1609
  def readonly?
1422
1610
  @readonly == true
1423
1611
  end
1424
1612
 
1425
- def readonly!
1613
+ def readonly! #:nodoc:
1426
1614
  @readonly = true
1427
1615
  end
1428
1616
 
1617
+ # Builds an XML document to represent the model. Some configuration is
1618
+ # availble through +options+, however more complicated cases should use
1619
+ # Builder.
1620
+ #
1621
+ # By default the generated XML document will include the processing
1622
+ # instruction and all object's attributes. For example:
1623
+ #
1624
+ # <?xml version="1.0" encoding="UTF-8"?>
1625
+ # <topic>
1626
+ # <title>The First Topic</title>
1627
+ # <author-name>David</author-name>
1628
+ # <id type="integer">1</id>
1629
+ # <approved type="boolean">false</approved>
1630
+ # <replies-count type="integer">0</replies-count>
1631
+ # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
1632
+ # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
1633
+ # <content>Have a nice day</content>
1634
+ # <author-email-address>david@loudthinking.com</author-email-address>
1635
+ # <parent-id></parent-id>
1636
+ # <last-read type="date">2004-04-15</last-read>
1637
+ # </topic>
1638
+ #
1639
+ # This behaviour can be controlled with :skip_attributes and :skip_instruct
1640
+ # for instance:
1641
+ #
1642
+ # topic.to_xml(:skip_instruct => true, :skip_attributes => [ :id, bonus_time, :written_on, replies_count ])
1643
+ #
1644
+ # <topic>
1645
+ # <title>The First Topic</title>
1646
+ # <author-name>David</author-name>
1647
+ # <approved type="boolean">false</approved>
1648
+ # <content>Have a nice day</content>
1649
+ # <author-email-address>david@loudthinking.com</author-email-address>
1650
+ # <parent-id></parent-id>
1651
+ # <last-read type="date">2004-04-15</last-read>
1652
+ # </topic>
1653
+ #
1654
+ # To include first level associations use :include
1655
+ #
1656
+ # firm.to_xml :include => [ :account, :clients ]
1657
+ #
1658
+ # <?xml version="1.0" encoding="UTF-8"?>
1659
+ # <firm>
1660
+ # <id type="integer">1</id>
1661
+ # <rating type="integer">1</rating>
1662
+ # <name>37signals</name>
1663
+ # <clients>
1664
+ # <client>
1665
+ # <rating type="integer">1</rating>
1666
+ # <name>Summit</name>
1667
+ # </client>
1668
+ # <client>
1669
+ # <rating type="integer">1</rating>
1670
+ # <name>Microsoft</name>
1671
+ # </client>
1672
+ # </clients>
1673
+ # <account>
1674
+ # <id type="integer">1</id>
1675
+ # <credit-limit type="integer">50</credit-limit>
1676
+ # </account>
1677
+ # </firm>
1678
+ def to_xml(options = {})
1679
+ options[:root] ||= self.class.to_s.underscore
1680
+ options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column
1681
+ root_only_or_except = { :only => options[:only], :except => options[:except] }
1682
+
1683
+ attributes_for_xml = attributes(root_only_or_except)
1684
+
1685
+ if include_associations = options.delete(:include)
1686
+ include_has_options = include_associations.is_a?(Hash)
1687
+
1688
+ for association in include_has_options ? include_associations.keys : Array(include_associations)
1689
+ association_options = include_has_options ? include_associations[association] : root_only_or_except
1690
+
1691
+ case self.class.reflect_on_association(association).macro
1692
+ when :has_many, :has_and_belongs_to_many
1693
+ records = send(association).to_a
1694
+ unless records.empty?
1695
+ attributes_for_xml[association] = records.collect do |record|
1696
+ record.attributes(association_options)
1697
+ end
1698
+ end
1699
+ when :has_one, :belongs_to
1700
+ if record = send(association)
1701
+ attributes_for_xml[association] = record.attributes(association_options)
1702
+ end
1703
+ end
1704
+ end
1705
+ end
1706
+
1707
+ attributes_for_xml.to_xml(options)
1708
+ end
1709
+
1429
1710
  private
1430
1711
  def create_or_update
1431
1712
  if new_record? then create else update end
@@ -1439,11 +1720,13 @@ module ActiveRecord #:nodoc:
1439
1720
  "WHERE #{self.class.primary_key} = #{quote(id)}",
1440
1721
  "#{self.class.name} Update"
1441
1722
  )
1723
+
1724
+ return true
1442
1725
  end
1443
1726
 
1444
1727
  # Creates a new record with values matching those of the instance attributes.
1445
1728
  def create
1446
- if self.id.nil? and connection.prefetch_primary_key?(self.class.table_name)
1729
+ if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
1447
1730
  self.id = connection.next_sequence_value(self.class.sequence_name)
1448
1731
  end
1449
1732
 
@@ -1456,6 +1739,8 @@ module ActiveRecord #:nodoc:
1456
1739
  )
1457
1740
 
1458
1741
  @new_record = false
1742
+
1743
+ return true
1459
1744
  end
1460
1745
 
1461
1746
  # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendent.
@@ -1478,19 +1763,19 @@ module ActiveRecord #:nodoc:
1478
1763
  # table with a master_id foreign key can instantiate master through Client#master.
1479
1764
  def method_missing(method_id, *args, &block)
1480
1765
  method_name = method_id.to_s
1481
- if @attributes.include?(method_name)
1766
+ if @attributes.include?(method_name) or
1767
+ (md = /\?$/.match(method_name) and
1768
+ @attributes.include?(method_name = md.pre_match))
1482
1769
  define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
1483
- read_attribute(method_name)
1770
+ md ? query_attribute(method_name) : read_attribute(method_name)
1484
1771
  elsif self.class.primary_key.to_s == method_name
1485
1772
  id
1486
- elsif md = /(=|\?|_before_type_cast)$/.match(method_name)
1773
+ elsif md = /(=|_before_type_cast)$/.match(method_name)
1487
1774
  attribute_name, method_type = md.pre_match, md.to_s
1488
1775
  if @attributes.include?(attribute_name)
1489
1776
  case method_type
1490
1777
  when '='
1491
1778
  write_attribute(attribute_name, args.first)
1492
- when '?'
1493
- query_attribute(attribute_name)
1494
1779
  when '_before_type_cast'
1495
1780
  read_attribute_before_type_cast(attribute_name)
1496
1781
  end
@@ -1530,8 +1815,9 @@ module ActiveRecord #:nodoc:
1530
1815
  # ActiveRecord::Base.generate_read_methods is set to true.
1531
1816
  def define_read_methods
1532
1817
  self.class.columns_hash.each do |name, column|
1533
- unless self.class.serialized_attributes[name] || respond_to_without_attributes?(name)
1534
- define_read_method(name.to_sym, name, column)
1818
+ unless self.class.serialized_attributes[name]
1819
+ define_read_method(name.to_sym, name, column) unless respond_to_without_attributes?(name)
1820
+ define_question_method(name) unless respond_to_without_attributes?("#{name}?")
1535
1821
  end
1536
1822
  end
1537
1823
  end
@@ -1540,14 +1826,28 @@ module ActiveRecord #:nodoc:
1540
1826
  def define_read_method(symbol, attr_name, column)
1541
1827
  cast_code = column.type_cast_code('v') if column
1542
1828
  access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
1543
-
1829
+
1544
1830
  unless attr_name.to_s == self.class.primary_key.to_s
1545
1831
  access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
1546
1832
  self.class.read_methods << attr_name
1547
1833
  end
1548
-
1834
+
1835
+ evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
1836
+ end
1837
+
1838
+ # Define an attribute ? method.
1839
+ def define_question_method(attr_name)
1840
+ unless attr_name.to_s == self.class.primary_key.to_s
1841
+ self.class.read_methods << "#{attr_name}?"
1842
+ end
1843
+
1844
+ evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end"
1845
+ end
1846
+
1847
+ # Evaluate the definition for an attribute reader or ? method
1848
+ def evaluate_read_method(attr_name, method_definition)
1549
1849
  begin
1550
- self.class.class_eval("def #{symbol}; #{access_code}; end")
1850
+ self.class.class_eval(method_definition)
1551
1851
  rescue SyntaxError => err
1552
1852
  self.class.read_methods.delete(attr_name)
1553
1853
  if logger
@@ -1764,4 +2064,4 @@ module ActiveRecord #:nodoc:
1764
2064
  value
1765
2065
  end
1766
2066
  end
1767
- end
2067
+ end