aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG136
-rw-r--r--activerecord/README.rdoc28
-rw-r--r--activerecord/RUNNING_UNIT_TESTS52
-rwxr-xr-xactiverecord/Rakefile47
-rw-r--r--activerecord/activerecord.gemspec7
-rw-r--r--activerecord/lib/active_record.rb9
-rw-r--r--activerecord/lib/active_record/aggregations.rb2
-rw-r--r--activerecord/lib/active_record/associations.rb157
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb30
-rw-r--r--activerecord/lib/active_record/associations/association.rb47
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb33
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb26
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb188
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb36
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb13
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb5
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb3
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb5
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb25
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb7
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb86
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb9
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb14
-rw-r--r--activerecord/lib/active_record/autosave_association.rb9
-rw-r--r--activerecord/lib/active_record/base.rb281
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb63
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb611
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb591
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb713
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb256
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb75
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb40
-rw-r--r--activerecord/lib/active_record/counter_cache.rb4
-rw-r--r--activerecord/lib/active_record/errors.rb15
-rw-r--r--activerecord/lib/active_record/fixtures.rb828
-rw-r--r--activerecord/lib/active_record/fixtures/file.rb65
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb34
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb6
-rw-r--r--activerecord/lib/active_record/migration.rb30
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb30
-rw-r--r--activerecord/lib/active_record/named_scope.rb21
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb30
-rw-r--r--activerecord/lib/active_record/observer.rb4
-rw-r--r--activerecord/lib/active_record/persistence.rb25
-rw-r--r--activerecord/lib/active_record/query_cache.rb20
-rw-r--r--activerecord/lib/active_record/railtie.rb4
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb15
-rw-r--r--activerecord/lib/active_record/railties/databases.rake64
-rw-r--r--activerecord/lib/active_record/reflection.rb97
-rw-r--r--activerecord/lib/active_record/relation.rb143
-rw-r--r--activerecord/lib/active_record/relation/batches.rb11
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb40
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb27
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb113
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/lib/active_record/result.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb6
-rw-r--r--activerecord/lib/active_record/serialization.rb44
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb46
-rw-r--r--activerecord/lib/active_record/session_store.rb50
-rw-r--r--activerecord/lib/active_record/test_case.rb24
-rw-r--r--activerecord/lib/active_record/timestamp.rb17
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb8
-rw-r--r--activerecord/lib/active_record/validations/associated.rb14
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb4
-rw-r--r--activerecord/lib/active_record/version.rb4
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/migration.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb6
-rw-r--r--activerecord/test/.gitignore1
-rw-r--r--activerecord/test/cases/adapter_test.rb42
-rw-r--r--activerecord/test/cases/adapters/firebird/migration_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb1
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb23
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb69
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb106
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb23
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb30
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb49
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb24
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb78
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb14
-rw-r--r--activerecord/test/cases/associations/eager_test.rb78
-rw-r--r--activerecord/test/cases/associations/extension_test.rb18
-rw-r--r--activerecord/test/cases/associations/habtm_join_table_test.rb9
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb33
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb152
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb59
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb97
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb3
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb25
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations_test.rb5
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb1
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb77
-rw-r--r--activerecord/test/cases/autosave_association_test.rb2
-rw-r--r--activerecord/test/cases/base_test.rb250
-rw-r--r--activerecord/test/cases/batches_test.rb43
-rw-r--r--activerecord/test/cases/calculations_test.rb22
-rw-r--r--activerecord/test/cases/column_definition_test.rb32
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb21
-rw-r--r--activerecord/test/cases/connection_management_test.rb41
-rw-r--r--activerecord/test/cases/connection_pool_test.rb4
-rw-r--r--activerecord/test/cases/finder_test.rb60
-rw-r--r--activerecord/test/cases/fixtures/file_test.rb83
-rw-r--r--activerecord/test/cases/fixtures_test.rb56
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb10
-rw-r--r--activerecord/test/cases/helper.rb38
-rw-r--r--activerecord/test/cases/i18n_test.rb1
-rw-r--r--activerecord/test/cases/identity_map_test.rb6
-rw-r--r--activerecord/test/cases/invalid_date_test.rb4
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb39
-rw-r--r--activerecord/test/cases/json_serialization_test.rb9
-rw-r--r--activerecord/test/cases/lifecycle_test.rb12
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb8
-rw-r--r--activerecord/test/cases/mass_assignment_security_test.rb377
-rw-r--r--activerecord/test/cases/method_scoping_test.rb8
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb23
-rw-r--r--activerecord/test/cases/migration_test.rb12
-rw-r--r--activerecord/test/cases/named_scope_test.rb32
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb5
-rw-r--r--activerecord/test/cases/persistence_test.rb37
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb2
-rw-r--r--activerecord/test/cases/primary_keys_test.rb20
-rw-r--r--activerecord/test/cases/query_cache_test.rb44
-rw-r--r--activerecord/test/cases/reflection_test.rb30
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb78
-rw-r--r--activerecord/test/cases/relation_test.rb2
-rw-r--r--activerecord/test/cases/relations_test.rb215
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb18
-rw-r--r--activerecord/test/cases/serialization_test.rb155
-rw-r--r--activerecord/test/cases/session_store/session_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb10
-rw-r--r--activerecord/test/cases/unconnected_test.rb2
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb134
-rw-r--r--activerecord/test/config.example.yml138
-rw-r--r--activerecord/test/connections/jdbc_jdbcderby/connection.rb18
-rw-r--r--activerecord/test/connections/jdbc_jdbch2/connection.rb18
-rw-r--r--activerecord/test/connections/jdbc_jdbchsqldb/connection.rb18
-rw-r--r--activerecord/test/connections/jdbc_jdbcmysql/connection.rb26
-rw-r--r--activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb26
-rw-r--r--activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb25
-rw-r--r--activerecord/test/connections/native_db2/connection.rb25
-rw-r--r--activerecord/test/connections/native_firebird/connection.rb26
-rw-r--r--activerecord/test/connections/native_frontbase/connection.rb27
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb25
-rw-r--r--activerecord/test/connections/native_mysql2/connection.rb25
-rw-r--r--activerecord/test/connections/native_openbase/connection.rb21
-rw-r--r--activerecord/test/connections/native_oracle/connection.rb35
-rw-r--r--activerecord/test/connections/native_postgresql/connection.rb21
-rw-r--r--activerecord/test/connections/native_sqlite3/connection.rb16
-rw-r--r--activerecord/test/connections/native_sqlite3_mem/connection.rb19
-rw-r--r--activerecord/test/connections/native_sybase/connection.rb23
-rw-r--r--activerecord/test/fixtures/categories_ordered.yml2
-rw-r--r--activerecord/test/fixtures/memberships.yml7
-rw-r--r--activerecord/test/fixtures/parrots.yml2
-rw-r--r--activerecord/test/fixtures/pirates.yml4
-rw-r--r--activerecord/test/fixtures/tasks.yml2
-rw-r--r--activerecord/test/models/aircraft.rb4
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--activerecord/test/models/bulb.rb29
-rw-r--r--activerecord/test/models/car.rb5
-rw-r--r--activerecord/test/models/comment.rb4
-rw-r--r--activerecord/test/models/company.rb12
-rw-r--r--activerecord/test/models/contact.rb13
-rw-r--r--activerecord/test/models/contract.rb15
-rw-r--r--activerecord/test/models/developer.rb70
-rw-r--r--activerecord/test/models/member.rb2
-rw-r--r--activerecord/test/models/membership.rb6
-rw-r--r--activerecord/test/models/person.rb8
-rw-r--r--activerecord/test/models/post.rb20
-rw-r--r--activerecord/test/models/topic.rb9
-rw-r--r--activerecord/test/models/toy.rb2
-rw-r--r--activerecord/test/schema/schema.rb18
-rw-r--r--activerecord/test/support/config.rb43
-rw-r--r--activerecord/test/support/connection.rb20
203 files changed, 6151 insertions, 3505 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 4dc652c08e..10ad35ae3c 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,8 +1,113 @@
-*Rails 3.1.0 (unreleased)*
+*Rails 3.1.1 (unreleased)*
+
+* Transactional fixtures enlist all active database connections. You can test
+ models on different connections without disabling transactional fixtures.
+
+ [Jeremy Kemper]
+
+* Add deprecation for the preload_associations method. Fixes #3022.
+
+ [Jon Leighton]
+
+* Don't require a DB connection when loading a model that uses set_primary_key. GH #2807.
+
+ [Jon Leighton]
+
+* Fix using select() with a habtm association, e.g. Person.friends.select(:name). GH #3030 and
+ #2923.
+
+ [Hendy Tanata]
+
+* Fix belongs_to polymorphic with custom primary key on target. GH #3104.
+
+ [Jon Leighton]
+
+* CollectionProxy#replace should change the DB records rather than just mutating the array.
+ Fixes #3020.
+
+ [Jon Leighton]
+
+* LRU cache in mysql and sqlite are now per-process caches.
+
+ * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache
+ keys are per process id.
+ * lib/active_record/connection_adapters/sqlite_adapter.rb: ditto
+
+* Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a
+ better approach over the old find_or_create_by dynamic methods because it's clearer which
+ arguments are used to find the record and which are used to create it:
+
+ User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson")
+
+ [Andrés Mejía]
+
+* Support bulk change_table in mysql2 adapter, as well as the mysql one. [Jon Leighton]
+
+* If multiple parameters are sent representing a date, and some are blank, the
+resulting object is nil. In previous releases those values defaulted to 1. This
+only affects existing but blank parameters, missing ones still raise an error.
+[Akira Matsuda]
+
+* ActiveRecord::Base.establish_connection now takes a string that contains
+a URI that specifies the connection configuration. For example:
+
+ ActiveRecord::Base.establish_connection 'postgres://localhost/foo'
+
+* Active Record's dynamic finder will now raise the error if you passing in less number of arguments than what you call in method signature.
+
+ So if you were doing this and expecting the second argument to be nil:
+
+ User.find_by_username_and_group("sikachu")
+
+ You'll now get `ArgumentError: wrong number of arguments (1 for 2).` You'll then have to do this:
+
+ User.find_by_username_and_group("sikachu", nil)
+
+ [Prem Sichanugrist]
+
+*Rails 3.1.0 (August 30, 2011)*
+
+* Add a proxy_association method to association proxies, which can be called by association
+ extensions to access information about the association. This replaces proxy_owner etc with
+ proxy_association.owner.
+
+ [Jon Leighton]
+
+* ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature.
+
+ Before: def build_association(*options)
+ After: def build_association(*options, &block)
+
+ Users who are redefining this method to extend functionality should ensure that the block is
+ passed through to ActiveRecord::Base#new.
+
+ This change is necessary to fix https://github.com/rails/rails/issues/1842.
+
+ [Jon Leighton]
+
+* AR#pluralize_table_names can be used to singularize/pluralize table name of an individual model:
+
+ class User < ActiveRecord::Base
+ self.pluralize_table_names = false
+ end
+
+ Previously this could only be set globally for all models through ActiveRecord::Base.pluralize_table_names. [Guillermo Iguaran]
+
+* Add block setting of attributes to singular associations:
+
+ class User < ActiveRecord::Base
+ has_one :account
+ end
+
+ user.build_account{ |a| a.credit_limit => 100.0 }
+
+ The block is called after the instance has been initialized. [Andrew White]
+
+* Add ActiveRecord::Base.attribute_names to return a list of attribute names. This will return an empty array if the model is abstract or table does not exists. [Prem Sichanugrist]
* CSV Fixtures are deprecated and support will be removed in Rails 3.2.0
-* AR#new, AR#create and AR#update_attributes all accept a second hash as option that allows you
+* AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes! all accept a second hash as option that allows you
to specify which role to consider when assigning attributes. This is built on top of ActiveModel's
new mass assignment capabilities:
@@ -14,7 +119,9 @@
Post.new(params[:post], :as => :admin)
assign_attributes() with similar API was also added and attributes=(params, guard) was deprecated.
-
+
+ Please note that this changes the method signatures for AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes!. If you have overwritten these methods you should update them accordingly.
+
[Josh Kalderimis]
* default_scope can take a block, lambda, or any other object which responds to `call` for lazy
@@ -218,7 +325,6 @@
def up
create_table :posts do |t|
t.belongs_to :user
-
t.timestamps
end
@@ -611,12 +717,12 @@ query.
* Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran]
- class Book < ActiveRecord::Base
- has_one :author
- has_many :pages
+ class Book < ActiveRecord::Base
+ has_one :author
+ has_many :pages
- accepts_nested_attributes_for :author, :pages
- end
+ accepts_nested_attributes_for :author, :pages
+ end
* Make after_save callbacks fire only if the record was successfully saved. #1735 [Michael Lovitt]
@@ -1036,7 +1142,7 @@ so newlines etc are escaped #10385 [Norbert Crombach]
"foo.bar" => "`foo`.`bar`"
* Complete the assimilation of Sexy Migrations from ErrFree [Chris Wanstrath, PJ Hyett]
- http://errtheblog.com/post/2381
+ http://errtheblog.com/post/2381
* Qualified column names work in hash conditions, like :conditions => { 'comments.created_at' => ... }. #9733 [Jack Danger Canty]
@@ -1152,7 +1258,7 @@ single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Bea
* Improve performance and functionality of the postgresql adapter. Closes #8049 [roderickvd]
- For more information see: http://dev.rubyonrails.org/ticket/8049
+ For more information see: http://dev.rubyonrails.org/ticket/8049
* Don't clobber includes passed to has_many.count [Jack Danger Canty]
@@ -1662,8 +1768,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing]
* Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples:
assert (Topic.exists?(:author_name => "David"))
- assert (Topic.exists?(:author_name => "Mary", :approved => true))
- assert (Topic.exists?(["parent_id = ?", 1]))
+ assert (Topic.exists?(:author_name => "Mary", :approved => true))
+ assert (Topic.exists?(["parent_id = ?", 1]))
* Schema dumper quotes date :default values. [Dave Thomas]
@@ -2119,8 +2225,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing]
* Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples:
assert (Topic.exists?(:author_name => "David"))
- assert (Topic.exists?(:author_name => "Mary", :approved => true))
- assert (Topic.exists?(["parent_id = ?", 1]))
+ assert (Topic.exists?(:author_name => "Mary", :approved => true))
+ assert (Topic.exists?(["parent_id = ?", 1]))
* Schema dumper quotes date :default values. [Dave Thomas]
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index a27640eac9..b5db57569c 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -3,7 +3,7 @@
Active Record connects classes to relational database tables to establish an
almost zero-configuration persistence layer for applications. The library
provides a base class that, when subclassed, sets up a mapping between the new
-class and an existing table in the database. In context of an application,
+class and an existing table in the database. In the context of an application,
these classes are commonly referred to as *models*. Models can also be
connected to other models; this is done by defining *associations*.
@@ -70,7 +70,7 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Validations.html]
-* Callbacks available for the entire life cycle (instantiation, saving, destroying, validating, etc.)
+* Callbacks available for the entire life cycle (instantiation, saving, destroying, validating, etc.).
class Person < ActiveRecord::Base
before_destroy :invalidate_payment_plan
@@ -80,18 +80,18 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Callbacks.html]
-* Observers that react to changes in a model
+* Observers that react to changes in a model.
class CommentObserver < ActiveRecord::Observer
def after_create(comment) # is called just after Comment#save
- CommentMailer.new_comment_email("david@loudthinking.com", comment)
+ CommentMailer.new_comment_email("david@loudthinking.com", comment).deliver
end
end
{Learn more}[link:classes/ActiveRecord/Observer.html]
-* Inheritance hierarchies
+* Inheritance hierarchies.
class Company < ActiveRecord::Base; end
class Firm < Company; end
@@ -101,7 +101,7 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Base.html]
-* Transactions
+* Transactions.
# Database transaction
Account.transaction do
@@ -112,7 +112,7 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html]
-* Reflections on columns, associations, and aggregations
+* Reflections on columns, associations, and aggregations.
reflection = Firm.reflect_on_association(:clients)
reflection.klass # => Client (class)
@@ -121,7 +121,7 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html]
-* Database abstraction through simple adapters
+* Database abstraction through simple adapters.
# connect to SQLite3
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "dbfile.sqlite3")
@@ -141,13 +141,13 @@ A short rundown of some of the major features:
SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].
-* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
+* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
-* Database agnostic schema management with Migrations
+* Database agnostic schema management with Migrations.
class AddSystemSettings < ActiveRecord::Migration
def self.up
@@ -197,13 +197,13 @@ Admit the Database:
== Download and installation
-The latest version of Active Record can be installed with Rubygems:
+The latest version of Active Record can be installed with RubyGems:
% [sudo] gem install activerecord
Source code can be downloaded as part of the Rails project on GitHub
-* https://github.com/rails/rails/tree/master/activerecord/
+* https://github.com/rails/rails/tree/master/activerecord
== License
@@ -215,8 +215,8 @@ Active Record is released under the MIT license.
API documentation is at
-* http://api.rubyonrails.com
+* http://api.rubyonrails.org
Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
-* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets
+* https://github.com/rails/rails/issues
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
index b3d376772e..6a2e23b01f 100644
--- a/activerecord/RUNNING_UNIT_TESTS
+++ b/activerecord/RUNNING_UNIT_TESTS
@@ -1,43 +1,39 @@
-== Creating the test database
+== Configure databases
-The default names for the test databases are "activerecord_unittest" and
-"activerecord_unittest2". If you want to use another database name, then be sure
-to update the connection adapter setups you want to test within
-test/connections/<your database>/connection.rb.
-When you have the database online, you can import the fixture tables with
-the test/schema/*.sql files.
+Copy test/config.example.yml to test/config.yml and edit as needed. Or just run the tests for
+the first time, which will do the copy automatically and use the default (sqlite3).
-Make sure that you create database objects with the same user that you specified in
-connection.rb otherwise (on Postgres, at least) tests for default values will fail.
+You can build postgres and mysql databases using the build_postgresql and build_mysql rake tasks.
-== Running with Rake
+== Running the tests
-The easiest way to run the unit tests is through Rake. The default task runs
-the entire test suite for all the adapters. You can also run the suite on just
-one adapter by using the tasks test_mysql, test_sqlite3, test_postgresql or any
-of the other test_ tasks. For more information, checkout the full array of rake
-tasks with "rake -T"
+You can run a particular test file from the command line, e.g.
-Rake can be found at http://rake.rubyforge.org
+ $ ruby -Itest test/cases/base_test.rb
-== Running by hand
+To run a specific test:
-Unit tests are located in test/cases directory. If you only want to run a single test suite,
-you can do so with:
+ $ ruby -Itest test/cases/base_test.rb -n test_something_works
- rake test_mysql TEST=test/cases/base_test.rb
+You can run with a database other than the default you set in test/config.yml, using the ARCONN
+environment variable:
-That'll run the base suite using the MySQL-Ruby adapter. Some tests rely on the schema
-being initialized - you can initialize the schema with:
+ $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
- rake test_mysql TEST=test/cases/aaa_create_tables_test.rb
- rake mysql:build_databases
+You can run all the tests for a given database via rake:
-To setup the testing environment for PostgreSQL use this command:
+ $ rake test_mysql
- rake postgresql:build_databases
+The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql.
-The incantation for running a particular test looks like this
+== Identity Map
- rake test TEST=test/cases/datatype_test_postgresql.rb TESTOPTS="-n test_timestamp_with_zone_values_without_rails_time_zone_support"
+By default the tests run with the Identity Map turned off. But all tests should pass whether or
+not the identity map is on or off. You can turn it on using the IM env variable:
+ $ IM=true ruby -Itest test/case/base_test.rb
+
+== Config file
+
+By default, the config file is expected to be at the path test/config.yml. You can specify a
+custom location with the ARCONFIG environment variable.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index e414c4fb1c..d769a73dba 100755
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,11 +1,10 @@
#!/usr/bin/env rake
require 'rake/testtask'
require 'rake/packagetask'
-require 'rake/gempackagetask'
+require 'rubygems/package_task'
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
-
-MYSQL_DB_USER = 'rails'
+require File.expand_path(File.dirname(__FILE__)) + "/test/support/config"
def run_without_aborting(*tasks)
errors = []
@@ -43,9 +42,8 @@ end
%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
Rake::TestTask.new("test_#{adapter}") { |t|
- connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}"
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- t.libs << "test" << connection_path
+ t.libs << 'test'
t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
|x| x =~ /\/adapters\//
} + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
@@ -55,21 +53,27 @@ end
}
task "isolated_test_#{adapter}" do
- connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}"
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- puts [adapter, adapter_short, connection_path].inspect
+ puts [adapter, adapter_short].inspect
ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
(Dir["test/cases/**/*_test.rb"].reject {
|x| x =~ /\/adapters\//
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(ruby, "-Ilib:test:#{connection_path}", file)
+ sh(ruby, "-Itest", file)
end or raise "Failures"
end
namespace adapter do
task :test => "test_#{adapter}"
task :isolated_test => "isolated_test_#{adapter}"
+
+ # Set the connection environment for the adapter
+ task(:env) { ENV['ARCONN'] = adapter }
end
+
+ # Make sure the adapter test evaluates the env setting task
+ task "test_#{adapter}" => "#{adapter}:env"
+ task "isolated_test_#{adapter}" => "#{adapter}:env"
end
rule '.sqlite3' do |t|
@@ -84,14 +88,16 @@ task :test_sqlite3 => [
namespace :mysql do
desc 'Build the MySQL test databases'
task :build_databases do
- %x( mysql --user=#{MYSQL_DB_USER} -e "create DATABASE activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{MYSQL_DB_USER} -e "create DATABASE activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ config = ARTest.config['connections']['mysql']
+ %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
end
desc 'Drop the MySQL test databases'
task :drop_databases do
- %x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest )
- %x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest2 )
+ config = ARTest.config['connections']['mysql']
+ %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
+ %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
end
desc 'Rebuild the MySQL test databases'
@@ -106,14 +112,16 @@ task :rebuild_mysql_databases => 'mysql:rebuild_databases'
namespace :postgresql do
desc 'Build the PostgreSQL test databases'
task :build_databases do
- %x( createdb -E UTF8 activerecord_unittest )
- %x( createdb -E UTF8 activerecord_unittest2 )
+ config = ARTest.config['connections']['postgresql']
+ %x( createdb -E UTF8 #{config['arunit']['database']} )
+ %x( createdb -E UTF8 #{config['arunit2']['database']} )
end
desc 'Drop the PostgreSQL test databases'
task :drop_databases do
- %x( dropdb activerecord_unittest )
- %x( dropdb activerecord_unittest2 )
+ config = ARTest.config['connections']['postgresql']
+ %x( dropdb #{config['arunit']['database']} )
+ %x( dropdb #{config['arunit2']['database']} )
end
desc 'Rebuild the PostgreSQL test databases'
@@ -152,8 +160,9 @@ namespace :frontbase do
DISCONNECT ALL;
)
end
- create_activerecord_unittest = build_frontbase_database['activerecord_unittest', File.join(SCHEMA_ROOT, 'frontbase.sql')]
- create_activerecord_unittest2 = build_frontbase_database['activerecord_unittest2', File.join(SCHEMA_ROOT, 'frontbase2.sql')]
+ config = ARTest.config['connections']['frontbase']
+ create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')]
+ create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')]
execute_frontbase_sql = Proc.new do |sql|
system(<<-SHELL)
/Library/FrontBase/bin/sql92 <<-SQL
@@ -171,7 +180,7 @@ task :rebuild_frontbase_databases => 'frontbase:rebuild_databases'
spec = eval(File.read('activerecord.gemspec'))
-Rake::GemPackageTask.new(spec) do |p|
+Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 3a5035305b..2de81c31a3 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -12,9 +12,8 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
s.homepage = 'http://www.rubyonrails.org'
- s.rubyforge_project = 'activerecord'
- s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*']
+ s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
s.extra_rdoc_files = %w( README.rdoc )
@@ -22,6 +21,6 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
- s.add_dependency('arel', '~> 2.1.0')
- s.add_dependency('tzinfo', '~> 0.3.27')
+ s.add_dependency('arel', '~> 2.2.1')
+ s.add_dependency('tzinfo', '~> 0.3.29')
end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 59cf42a377..132dc12680 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -21,13 +21,6 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-
-activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
-$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
-
-activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
-$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
-
require 'active_support'
require 'active_support/i18n'
require 'active_model'
@@ -41,6 +34,7 @@ module ActiveRecord
eager_autoload do
autoload :ActiveRecordError, 'active_record/errors'
autoload :ConnectionNotEstablished, 'active_record/errors'
+ autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter'
autoload :Aggregations
autoload :Associations
@@ -71,6 +65,7 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Reflection
+ autoload :Result
autoload :Schema
autoload :SchemaDumper
autoload :Serialization
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 90d3b58c78..81ddbba51e 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -176,7 +176,7 @@ module ActiveRecord
# order in which mappings are defined determine the order in which attributes are sent to the
# value class constructor.
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
- # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
# mapped attributes.
# This defaults to +false+.
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 08fb6bf3c4..0952ea2829 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -33,7 +33,7 @@ module ActiveRecord
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
end
end
@@ -48,7 +48,7 @@ module ActiveRecord
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -76,12 +76,6 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
- end
- end
-
class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
@@ -96,7 +90,7 @@ module ActiveRecord
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
@@ -197,7 +191,7 @@ module ActiveRecord
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
- # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options),</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.all(options),</tt>
# <tt>Project#milestones.build, Project#milestones.create</tt>
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
@@ -426,7 +420,7 @@ module ActiveRecord
# end
# end
#
- # person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
+ # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
# person.first_name # => "David"
# person.last_name # => "Heinemeier Hansson"
#
@@ -457,20 +451,27 @@ module ActiveRecord
# has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
# end
#
- # Some extensions can only be made to work with knowledge of the association proxy's internals.
- # Extensions can access relevant state using accessors on the association proxy:
+ # Some extensions can only be made to work with knowledge of the association's internals.
+ # Extensions can access relevant state using the following methods (where +items+ is the
+ # name of the association):
#
- # * +proxy_owner+ - Returns the object the association is part of.
- # * +proxy_reflection+ - Returns the reflection object that describes the association.
- # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or
+ # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
+ # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
+ # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or
# the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
#
+ # However, inside the actual extension code, you will not have access to the <tt>record</tt> as
+ # above. In this case, you can access <tt>proxy_association</tt>. For example,
+ # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return
+ # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
+ # association extensions.
+ #
# === Association Join Models
#
# Has Many associations can be configured with the <tt>:through</tt> option to use an
- # explicit join model to retrieve the data. This operates similarly to a
- # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
- # callbacks, and extra attributes on the join model. Consider the following schema:
+ # explicit join model to retrieve the data. This operates similarly to a
+ # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
+ # callbacks, and extra attributes on the join model. Consider the following schema:
#
# class Author < ActiveRecord::Base
# has_many :authorships
@@ -482,7 +483,7 @@ module ActiveRecord
# belongs_to :book
# end
#
- # @author = Author.find :first
+ # @author = Author.first
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
# @author.books # selects all books by using the Authorship join model
#
@@ -502,7 +503,7 @@ module ActiveRecord
# belongs_to :client
# end
#
- # @firm = Firm.find :first
+ # @firm = Firm.first
# @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model
#
@@ -527,7 +528,7 @@ module ActiveRecord
# @group.avatars # selects all avatars by going through the User join model.
#
# An important caveat with going through +has_one+ or +has_many+ associations on the
- # join model is that these associations are *read-only*. For example, the following
+ # join model is that these associations are *read-only*. For example, the following
# would not work following the previous example:
#
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
@@ -595,7 +596,7 @@ module ActiveRecord
# === Polymorphic Associations
#
# Polymorphic associations on models are not restricted on what types of models they
- # can be associated with. Rather, they specify an interface that a +has_many+ association
+ # can be associated with. Rather, they specify an interface that a +has_many+ association
# must adhere to.
#
# class Asset < ActiveRecord::Base
@@ -609,7 +610,7 @@ module ActiveRecord
# @asset.attachable = @post
#
# This works by using a type column in addition to a foreign key to specify the associated
- # record. In the Asset example, you'd need an +attachable_id+ integer column and an
+ # record. In the Asset example, you'd need an +attachable_id+ integer column and an
# +attachable_type+ string column.
#
# Using polymorphic associations in combination with single table inheritance (STI) is
@@ -665,7 +666,7 @@ module ActiveRecord
#
# Consider the following loop using the class above:
#
- # for post in Post.all
+ # Post.all.each do |post|
# puts "Post: " + post.title
# puts "Written by: " + post.author.name
# puts "Last comment on: " + post.comments.first.created_on
@@ -674,7 +675,7 @@ module ActiveRecord
# To iterate over these one hundred posts, we'll generate 201 database queries. Let's
# first just optimize it for retrieving the author:
#
- # for post in Post.find(:all, :include => :author)
+ # Post.includes(:author).each do |post|
#
# This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
# symbol. After loading the posts, find will collect the +author_id+ from each one and load
@@ -683,7 +684,7 @@ module ActiveRecord
#
# We can improve upon the situation further by referencing both associations in the finder with:
#
- # for post in Post.find(:all, :include => [ :author, :comments ])
+ # Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
# to 3. More generally the number of queries will be 1 plus the number of associations
@@ -691,7 +692,7 @@ module ActiveRecord
#
# To include a deep hierarchy of associations, use a hash:
#
- # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
+ # Post.includes(:author, {:comments => {:author => :gravatar}}).each do |post|
#
# That'll grab not only all the comments but all their authors and gravatar pictures.
# You can mix and match symbols, arrays and hashes in any combination to describe the
@@ -719,13 +720,13 @@ module ActiveRecord
# <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
#
# If you do want eager load only some members of an association it is usually more natural
- # to <tt>:include</tt> an association which has conditions defined on it:
+ # to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true]
# end
#
- # Post.find(:all, :include => :approved_comments)
+ # Post.includes(:approved_comments)
#
# This will load posts and eager load the +approved_comments+ association, which contains
# only those comments that have been approved.
@@ -737,10 +738,10 @@ module ActiveRecord
# has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10
# end
#
- # Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments.
+ # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
#
# When eager loaded, conditions are interpolated in the context of the model class, not
- # the model instance. Conditions are lazily interpolated before the actual model exists.
+ # the model instance. Conditions are lazily interpolated before the actual model exists.
#
# Eager loading is supported with polymorphic associations.
#
@@ -750,7 +751,7 @@ module ActiveRecord
#
# A call that tries to eager load the addressable model
#
- # Address.find(:all, :include => :addressable)
+ # Address.includes(:addressable)
#
# This will execute one query to load the addresses and load the addressables with one
# query per addressable type.
@@ -764,47 +765,47 @@ module ActiveRecord
# == Table Aliasing
#
# Active Record uses table aliasing in the case that a table is referenced multiple times
- # in a join. If a table is referenced only once, the standard table name is used. The
+ # in a join. If a table is referenced only once, the standard table name is used. The
# second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
# Indexes are appended for any more successive uses of the table name.
#
- # Post.find :all, :joins => :comments
+ # Post.joins(:comments)
# # => SELECT ... FROM posts INNER JOIN comments ON ...
- # Post.find :all, :joins => :special_comments # STI
+ # Post.joins(:special_comments) # STI
# # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
- # Post.find :all, :joins => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
+ # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
#
# Acts as tree example:
#
- # TreeMixin.find :all, :joins => :children
+ # TreeMixin.joins(:children)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # TreeMixin.find :all, :joins => {:children => :parent}
+ # TreeMixin.joins(:children => :parent)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
- # TreeMixin.find :all, :joins => {:children => {:parent => :children}}
+ # TreeMixin.joins(:children => {:parent => :children})
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# INNER JOIN mixins childrens_mixins_2
#
# Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
#
- # Post.find :all, :joins => :categories
+ # Post.joins(:categories)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # Post.find :all, :joins => {:categories => :posts}
+ # Post.joins(:categories => :posts)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
- # Post.find :all, :joins => {:categories => {:posts => :categories}}
+ # Post.joins(:categories => {:posts => :categories})
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
#
- # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table
+ # If you wish to specify your own custom joins using <tt>joins</tt> method, those table
# names will take precedence over the eager associations:
#
- # Post.find :all, :joins => :comments, :joins => "inner join comments ..."
+ # Post.joins(:comments).joins("inner join comments ...")
# # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
- # Post.find :all, :joins => [:comments, :special_comments], :joins => "inner join comments ..."
+ # Post.joins(:comments, :special_comments).joins("inner join comments ...")
# # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
# INNER JOIN comments special_comments_posts ...
# INNER JOIN comments ...
@@ -846,7 +847,7 @@ module ActiveRecord
# == Bi-directional associations
#
# When you specify an association there is usually an association on the associated model
- # that specifies the same relationship in reverse. For example, with the following models:
+ # that specifies the same relationship in reverse. For example, with the following models:
#
# class Dungeon < ActiveRecord::Base
# has_many :traps
@@ -863,9 +864,9 @@ module ActiveRecord
#
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
- # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
# Active Record doesn't know anything about these inverse relationships and so no object
- # loading optimisation is possible. For example:
+ # loading optimization is possible. For example:
#
# d = Dungeon.first
# t = d.traps.first
@@ -875,8 +876,8 @@ module ActiveRecord
#
# The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
# the same object data from the database, but are actually different in-memory copies
- # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
- # Active Record about inverse relationships and it will optimise object loading. For
+ # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
+ # Active Record about inverse relationships and it will optimise object loading. For
# example, if we changed our model definitions to:
#
# class Dungeon < ActiveRecord::Base
@@ -1036,7 +1037,7 @@ module ActiveRecord
# === Example
#
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => ["firm_id = ?", id]</tt>)
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients=</tt>
@@ -1059,7 +1060,7 @@ module ActiveRecord
# specify it with this option.
# [:conditions]
# Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from
+ # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from
# the association are scoped if a hash is used.
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published
# posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
@@ -1074,10 +1075,11 @@ module ActiveRecord
# Specify the method that returns the primary key used for the association. By default this is +id+.
# [:dependent]
# If set to <tt>:destroy</tt> all the associated objects are destroyed
- # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
- # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
+ # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
+ # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
- # <tt>:restrict</tt> this object cannot be deleted if it has any associated object.
+ # <tt>:restrict</tt> this object raises an <tt>ActiveRecord::DeleteRestrictionError</tt> exception and
+ # cannot be deleted if it has any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a +belongs_to+, and the records which get deleted are the join records, rather than
@@ -1085,7 +1087,8 @@ module ActiveRecord
#
# [:finder_sql]
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
- # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+
+ # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is
+ # required. Note: When this option is used, +find_in_collection+
# is _not_ added.
# [:counter_sql]
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
@@ -1160,11 +1163,14 @@ module ActiveRecord
# has_many :tags, :as => :taggable
# has_many :reports, :readonly => true
# has_many :subscribers, :through => :subscriptions, :source => :user
- # has_many :subscribers, :class_name => "Person", :finder_sql =>
- # 'SELECT DISTINCT people.* ' +
- # 'FROM people p, post_subscriptions ps ' +
- # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
- # 'ORDER BY p.first_name'
+ # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new {
+ # %Q{
+ # SELECT DISTINCT people.*
+ # FROM people p, post_subscriptions ps
+ # WHERE ps.post_id = #{id} AND ps.person_id = p.id
+ # ORDER BY p.first_name
+ # }
+ # }
def has_many(name, options = {}, &extension)
Builder::HasMany.build(self, name, options, &extension)
end
@@ -1199,7 +1205,7 @@ module ActiveRecord
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(:conditions => "account_id = #{id}")</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
@@ -1226,7 +1232,8 @@ module ActiveRecord
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
# If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+.
- # Also, association is assigned.
+ # Also, association is assigned. If set to <tt>:restrict</tt> this object raises an
+ # <tt>ActiveRecord::DeleteRestrictionError</tt> exception and cannot be deleted if it has any associated object.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
@@ -1242,7 +1249,7 @@ module ActiveRecord
# you want to do a join but not include the joined columns. Do not forget to include the
# primary and foreign keys, otherwise it will raise an error.
# [:through]
- # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
# or <tt>belongs_to</tt> association on the join model.
@@ -1264,7 +1271,7 @@ module ActiveRecord
# By default, only save the associated object if it's a new record.
# [:inverse_of]
# Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
+ # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
@@ -1322,7 +1329,7 @@ module ActiveRecord
#
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
+ # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
# if the real class name is Person, you'll have to specify it with this option.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
@@ -1379,10 +1386,10 @@ module ActiveRecord
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to now)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
- # will be updated with the current time instead of the updated_at/on attribute.
+ # will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
# Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
- # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
+ # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
@@ -1402,15 +1409,15 @@ module ActiveRecord
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
- # intermediate join table. Unless the join table is explicitly specified as an option, it is
+ # intermediate join table. Unless the join table is explicitly specified as an option, it is
# guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" outranks "P".
- # Note that this precedence is calculated using the <tt><</tt> operator for String. This
+ # Note that this precedence is calculated using the <tt><</tt> operator for String. This
# means that if the strings are of different lengths, and the strings are equal when compared
# up to the shortest length, then the longer string is considered of higher
- # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
+ # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
- # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
+ # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
#
# The join table should not have a primary key or a model associated with it. You must manually generate the
@@ -1512,7 +1519,7 @@ module ActiveRecord
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are
+ # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are
# scoped if a hash is used.
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
@@ -1568,7 +1575,7 @@ module ActiveRecord
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
# has_and_belongs_to_many :categories, :readonly => true
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
- # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
+ # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}"
def has_and_belongs_to_many(name, options = {}, &extension)
Builder::HasAndBelongsToMany.build(self, name, options, &extension)
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 634dee2289..0248c7483c 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -9,7 +9,7 @@ module ActiveRecord
# table_joins is an array of arel joins which might conflict with the aliases we assign here
def initialize(table_joins = [])
- @aliases = Hash.new
+ @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
@table_joins = table_joins
end
@@ -26,8 +26,6 @@ module ActiveRecord
def aliased_name_for(table_name, aliased_name = nil)
aliased_name ||= table_name
- initialize_count_for(table_name) if aliases[table_name].nil?
-
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
@@ -36,8 +34,6 @@ module ActiveRecord
# Otherwise, we need to use an alias
aliased_name = connection.table_alias_for(aliased_name)
- initialize_count_for(aliased_name) if aliases[aliased_name].nil?
-
# Update the count
aliases[aliased_name] += 1
@@ -49,32 +45,30 @@ module ActiveRecord
end
end
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
- end
-
private
- def initialize_count_for(name)
- aliases[name] = 0
+ def initial_count_for(name)
+ return 0 if Arel::Table === table_joins
- unless Arel::Table === table_joins
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
- aliases[name] += table_joins.map { |join|
+ counts = table_joins.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
- }.sum
+ else
+ join.left.table_name == name ? 1 : 0
+ end
end
- aliases[name]
+ counts.sum
end
def truncate(name)
- name[0..connection.table_alias_length-3]
+ name.slice(0, connection.table_alias_length - 2)
end
def connection
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 687b668634..d1e3ff8e38 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -30,7 +30,7 @@ module ActiveRecord
@updated = false
reset
- construct_scope
+ reset_scope
end
# Returns the name of the table of the related class:
@@ -51,7 +51,7 @@ module ActiveRecord
# Reloads the \target and returns +self+ on success.
def reload
reset
- construct_scope
+ reset_scope
load_target
self unless target.nil?
end
@@ -84,21 +84,25 @@ module ActiveRecord
end
def scoped
- target_scope.merge(@association_scope)
+ target_scope.merge(association_scope)
end
- # Construct the scope for this association.
+ # The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
# scoped method is called. This is because at that point the call may be surrounded
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
- def construct_scope
+ def association_scope
if klass
- @association_scope = AssociationScope.new(self).scope
+ @association_scope ||= AssociationScope.new(self).scope
end
end
+ def reset_scope
+ @association_scope = nil
+ end
+
# Set the inverse association, if possible
def set_inverse_instance(record)
if record && invertible_for?(record)
@@ -141,26 +145,26 @@ module ActiveRecord
@target ||= find_target
end
end
- loaded!
+ loaded! unless loaded?
target
rescue ActiveRecord::RecordNotFound
reset
end
+ def interpolate(sql, record = nil)
+ if sql.respond_to?(:to_proc)
+ owner.send(:instance_exec, record, &sql)
+ else
+ sql
+ end
+ end
+
private
def find_target?
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
end
- def interpolate(sql, record = nil)
- if sql.respond_to?(:to_proc)
- owner.send(:instance_exec, record, &sql)
- else
- sql
- end
- end
-
def creation_attributes
attributes = {}
@@ -177,9 +181,7 @@ module ActiveRecord
# Sets the owner attributes on the given record
def set_owner_attributes(record)
- if owner.persisted?
- creation_attributes.each { |key, value| record[key] = value }
- end
+ creation_attributes.each { |key, value| record[key] = value }
end
# Should be true if there is a foreign key present on the owner which
@@ -226,6 +228,15 @@ module ActiveRecord
def association_class
@reflection.klass
end
+
+ def build_record(attributes, options)
+ reflection.build_association(attributes, options) do |record|
+ record.assign_attributes(
+ create_scope.except(*record.changed),
+ :without_protection => true
+ )
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index ab102b2b8f..6cc401e6cc 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -42,10 +42,6 @@ module ActiveRecord
select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
end
- if reflection.macro == :has_and_belongs_to_many
- select_value ||= reflection.klass.arel_table[Arel.star]
- end
-
select_value
end
@@ -60,7 +56,7 @@ module ActiveRecord
scope = scope.joins(join(
join_table,
- table[reflection.active_record_primary_key].
+ table[reflection.association_primary_key].
eq(join_table[reflection.association_foreign_key])
))
@@ -68,17 +64,28 @@ module ActiveRecord
end
if reflection.source_macro == :belongs_to
- key = reflection.association_primary_key
+ if reflection.options[:polymorphic]
+ key = reflection.association_primary_key(klass)
+ else
+ key = reflection.association_primary_key
+ end
+
foreign_key = reflection.foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
+ conditions = self.conditions[i]
+
if reflection == chain.last
scope = scope.where(table[key].eq(owner[foreign_key]))
- conditions[i].each do |condition|
+ if reflection.type
+ scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
+ end
+
+ conditions.each do |condition|
if options[:through] && condition.is_a?(Hash)
condition = { table.name => condition }
end
@@ -87,12 +94,16 @@ module ActiveRecord
end
else
constraint = table[key].eq(foreign_table[foreign_key])
- join = join(foreign_table, constraint)
- scope = scope.joins(join)
+ if reflection.type
+ type = chain[i + 1].klass.base_class.name
+ constraint = constraint.and(table[reflection.type].eq(type))
+ end
+
+ scope = scope.joins(join(foreign_table, constraint))
- unless conditions[i].empty?
- scope = scope.where(sanitize(conditions[i], table))
+ unless conditions.empty?
+ scope = scope.where(sanitize(conditions, table))
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index c263edd2c6..97f531d064 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -20,6 +20,10 @@ module ActiveRecord
private
+ def find_target?
+ !loaded? && foreign_key_present? && klass
+ end
+
def update_counters(record)
counter_cache_name = reflection.counter_cache_column
@@ -37,11 +41,15 @@ module ActiveRecord
# Checks whether record is different to the current target, without loading it
def different_target?(record)
record.nil? && owner[reflection.foreign_key] ||
- record.id != owner[reflection.foreign_key]
+ record && record.id != owner[reflection.foreign_key]
end
def replace_keys(record)
- owner[reflection.foreign_key] = record && record[reflection.association_primary_key]
+ if record
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ else
+ owner[reflection.foreign_key] = nil
+ end
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 1ca448236e..2ee5dbbd70 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -2,6 +2,11 @@ module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
+ def klass
+ type = owner[reflection.foreign_type]
+ type.presence && type.constantize
+ end
+
private
def replace_keys(record)
@@ -17,11 +22,6 @@ module ActiveRecord
reflection.polymorphic_inverse_of(record.class)
end
- def klass
- type = owner[reflection.foreign_type]
- type && type.constantize
- end
-
def raise_on_type_mismatch(record)
# A polymorphic association cannot have a type mismatch, by definition
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 4b48757da7..30fc44b4c2 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -7,24 +7,22 @@ module ActiveRecord::Associations::Builder
def build
reflection = super
check_validity(reflection)
- define_after_destroy_method
+ define_destroy_hook
reflection
end
private
- def define_after_destroy_method
+ def define_destroy_hook
name = self.name
- model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
- def #{after_destroy_method_name}
- association(#{name.to_sym.inspect}).delete_all
- end
- eoruby
- model.after_destroy after_destroy_method_name
- end
-
- def after_destroy_method_name
- "has_and_belongs_to_many_after_destroy_for_#{name}"
+ model.send(:include, Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(#{name.to_sym.inspect}).delete_all
+ super
+ end
+ RUBY
+ })
end
# TODO: These checks should probably be moved into the Reflection, and we should not be
@@ -39,10 +37,6 @@ module ActiveRecord::Associations::Builder
model.send(:undecorated_table_name, model.to_s),
model.send(:undecorated_table_name, reflection.class_name)
)
-
- if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false)
- raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
- end
end
# Generates a join table name from two provided table names.
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 06a414b874..0cbbba041a 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -16,16 +16,16 @@ module ActiveRecord::Associations::Builder
def define_constructors
name = self.name
- model.redefine_method("build_#{name}") do |*params|
- association(name).build(*params)
+ model.redefine_method("build_#{name}") do |*params, &block|
+ association(name).build(*params, &block)
end
- model.redefine_method("create_#{name}") do |*params|
- association(name).create(*params)
+ model.redefine_method("create_#{name}") do |*params, &block|
+ association(name).create(*params, &block)
end
- model.redefine_method("create_#{name}!") do |*params|
- association(name).create!(*params)
+ model.redefine_method("create_#{name}!") do |*params, &block|
+ association(name).create!(*params, &block)
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index ccd6b82217..cec876149c 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module Associations
# = Active Record Association Collection
#
- # AssociationCollection is an abstract class that provides common stuff to
+ # CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
# collections. See the class hierarchy in AssociationProxy.
#
@@ -50,7 +50,7 @@ module ActiveRecord
else
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- scoped.select(column).except(:includes).map! do |record|
+ scoped.select(column).map! do |record|
record.send(reflection.association_primary_key)
end
end
@@ -78,10 +78,14 @@ module ActiveRecord
end
def find(*args)
- if options[:finder_sql]
- find_by_scan(*args)
+ if block_given?
+ load_target.find(*args) { |*block_args| yield(*block_args) }
else
- scoped.find(*args)
+ if options[:finder_sql]
+ find_by_scan(*args)
+ else
+ scoped.find(*args)
+ end
end
end
@@ -104,44 +108,23 @@ module ActiveRecord
end
def create(attributes = {}, options = {}, &block)
- unless owner.persisted?
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
- end
-
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr, options, &block) }
- else
- transaction do
- add_to_target(build_record(attributes, options)) do |record|
- yield(record) if block_given?
- insert_record(record)
- end
- end
- end
+ create_record(attributes, options, &block)
end
- def create!(attrs = {}, options = {}, &block)
- record = create(attrs, options, &block)
- Array.wrap(record).each(&:save!)
- record
+ def create!(attributes = {}, options = {}, &block)
+ create_record(attributes, options, true, &block)
end
- # Add +records+ to this association. Returns +self+ so method calls may be chained.
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
def concat(*records)
- result = true
load_target if owner.new_record?
- transaction do
- records.flatten.each do |record|
- raise_on_type_mismatch(record)
- add_to_target(record) do |r|
- result &&= insert_record(record) unless owner.new_record?
- end
- end
+ if owner.new_record?
+ concat_records(records)
+ else
+ transaction { concat_records(records) }
end
-
- result && records
end
# Starts a transaction in the association class's database connection.
@@ -310,14 +293,10 @@ module ActiveRecord
other_array.each { |val| raise_on_type_mismatch(val) }
original_target = load_target.dup
- transaction do
- delete(target - other_array)
-
- unless concat(other_array - target)
- @target = original_target
- raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
- "new records could not be saved."
- end
+ if owner.new_record?
+ replace_records(other_array, original_target)
+ else
+ transaction { replace_records(other_array, original_target) }
end
end
@@ -336,15 +315,7 @@ module ActiveRecord
def load_target
if find_target?
- targets = []
-
- begin
- targets = find_target
- rescue ActiveRecord::RecordNotFound
- reset
- end
-
- @target = merge_target_lists(targets, target)
+ @target = merge_target_lists(find_target, target)
end
loaded!
@@ -387,7 +358,7 @@ module ActiveRecord
if options[:finder_sql]
reflection.klass.find_by_sql(custom_finder_sql)
else
- find(:all)
+ scoped.all
end
records = options[:uniq] ? uniq(records) : records
@@ -395,37 +366,65 @@ module ActiveRecord
records
end
- def merge_target_lists(loaded, existing)
- return loaded if existing.empty?
- return existing if loaded.empty?
-
- loaded.map do |f|
- i = existing.index(f)
- if i
- existing.delete_at(i).tap do |t|
- keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
- # FIXME: this call to attributes causes many NoMethodErrors
- attributes = f.attributes
- (attributes.keys - keys).each do |k|
- t.send("#{k}=", attributes[k])
- end
+ # We have some records loaded from the database (persisted) and some that are
+ # in-memory (memory). The same record may be represented in the persisted array
+ # and in the memory array.
+ #
+ # So the task of this method is to merge them according to the following rules:
+ #
+ # * The final array must not have duplicates
+ # * The order of the persisted array is to be preserved
+ # * Any changes made to attributes on objects in the memory array are to be preserved
+ # * Otherwise, attributes should have the value found in the database
+ def merge_target_lists(persisted, memory)
+ return persisted if memory.empty?
+ return memory if persisted.empty?
+
+ persisted.map! do |record|
+ # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns
+ # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9.
+ mem_index = memory.index(record)
+
+ if mem_index
+ mem_record = memory.delete_at(mem_index)
+
+ (record.attribute_names - mem_record.changes.keys).each do |name|
+ mem_record[name] = record[name]
end
+
+ mem_record
else
- f
+ record
end
- end + existing
+ end
+
+ persisted + memory
+ end
+
+ def create_record(attributes, options, raise = false, &block)
+ unless owner.persisted?
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
+ end
+
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create_record(attr, options, raise, &block) }
+ else
+ transaction do
+ add_to_target(build_record(attributes, options)) do |record|
+ yield(record) if block_given?
+ insert_record(record, true, raise)
+ end
+ end
+ end
end
# Do the relevant stuff to insert the given record into the association collection.
- def insert_record(record, validate = true)
+ def insert_record(record, validate = true, raise = false)
raise NotImplementedError
end
- def build_record(attributes, options)
- record = reflection.build_association
- record.assign_attributes(scoped.scope_for_create, :without_protection => true)
- record.assign_attributes(attributes, options)
- record
+ def create_scope
+ scoped.scope_for_create.stringify_keys
end
def delete_or_destroy(records, method)
@@ -433,14 +432,20 @@ module ActiveRecord
records.each { |record| raise_on_type_mismatch(record) }
existing_records = records.reject { |r| r.new_record? }
- transaction do
- records.each { |record| callback(:before_remove, record) }
+ if existing_records.empty?
+ remove_records(existing_records, records, method)
+ else
+ transaction { remove_records(existing_records, records, method) }
+ end
+ end
+
+ def remove_records(existing_records, records, method)
+ records.each { |record| callback(:before_remove, record) }
- delete_records(existing_records, method) if existing_records.any?
- records.each { |record| target.delete(record) }
+ delete_records(existing_records, method) if existing_records.any?
+ records.each { |record| target.delete(record) }
- records.each { |record| callback(:after_remove, record) }
- end
+ records.each { |record| callback(:after_remove, record) }
end
# Delete the given records from the association, using one of the methods :destroy,
@@ -449,6 +454,29 @@ module ActiveRecord
raise NotImplementedError
end
+ def replace_records(new_target, original_target)
+ delete(target - new_target)
+
+ unless concat(new_target - target)
+ @target = original_target
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
+ "new records could not be saved."
+ end
+ end
+
+ def concat_records(records)
+ result = true
+
+ records.flatten.each do |record|
+ raise_on_type_mismatch(record)
+ add_to_target(record) do |r|
+ result &&= insert_record(record) unless owner.new_record?
+ end
+ end
+
+ result && records
+ end
+
def callback(method, record)
callbacks_for(method).each do |callback|
case callback
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index adfc71d435..3181ca9a32 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -12,7 +12,7 @@ module ActiveRecord
# has_many :posts
# end
#
- # blog = Blog.find(:first)
+ # blog = Blog.first
#
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
@@ -46,7 +46,7 @@ module ActiveRecord
delegate :select, :find, :first, :last,
:build, :create, :create!,
- :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq,
+ :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
:sum, :count, :size, :length, :empty?,
:any?, :many?, :include?,
:to => :@association
@@ -56,23 +56,29 @@ module ActiveRecord
Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
end
- def respond_to?(*args)
+ alias_method :new, :build
+
+ def proxy_association
+ @association
+ end
+
+ def respond_to?(name, include_private = false)
super ||
- (load_target && target.respond_to?(*args)) ||
- @association.klass.respond_to?(*args)
+ (load_target && target.respond_to?(name, include_private)) ||
+ proxy_association.klass.respond_to?(name, include_private)
end
def method_missing(method, *args, &block)
match = DynamicFinderMatch.match(method)
if match && match.instantiator?
- record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
- @association.send :set_owner_attributes, r
- @association.send :add_to_target, r
+ send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
+ proxy_association.send :set_owner_attributes, r
+ proxy_association.send :add_to_target, r
yield(r) if block_given?
end
end
- if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method))
+ if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
if load_target
if target.respond_to?(method)
target.send(method, *args, &block)
@@ -102,7 +108,7 @@ module ActiveRecord
alias_method :to_a, :to_ary
def <<(*records)
- @association.concat(records) && self
+ proxy_association.concat(records) && self
end
alias_method :push, :<<
@@ -112,17 +118,9 @@ module ActiveRecord
end
def reload
- @association.reload
+ proxy_association.reload
self
end
-
- def new(*args, &block)
- if @association.is_a?(HasManyThroughAssociation)
- @association.build(*args, &block)
- else
- method_missing(:new, *args, &block)
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index 217213808b..1f917f58f2 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -9,8 +9,14 @@ module ActiveRecord
super
end
- def insert_record(record, validate = true)
- return if record.new_record? && !record.save(:validate => validate)
+ def insert_record(record, validate = true, raise = false)
+ if record.new_record?
+ if raise
+ record.save!(:validate => validate)
+ else
+ return unless record.save(:validate => validate)
+ end
+ end
if options[:insert_sql]
owner.connection.insert(interpolate(options[:insert_sql], record))
@@ -20,7 +26,7 @@ module ActiveRecord
join_table[reflection.association_foreign_key] => record.id
)
- owner.connection.insert stmt.to_sql
+ owner.connection.insert stmt
end
record
@@ -40,7 +46,7 @@ module ActiveRecord
stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
).compile_delete
- owner.connection.delete stmt.to_sql
+ owner.connection.delete stmt
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 78c5c4b870..50ee60284c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -7,9 +7,14 @@ module ActiveRecord
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
- def insert_record(record, validate = true)
+ def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
- record.save(:validate => validate)
+
+ if raise
+ record.save!(:validate => validate)
+ else
+ record.save(:validate => validate)
+ end
end
private
@@ -18,7 +23,7 @@ module ActiveRecord
#
# If the association has a counter cache it gets that value. Otherwise
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
- # there's one. Some configuration options like :group make it impossible
+ # there's one. Some configuration options like :group make it impossible
# to do an SQL count, in those cases the array count will be used.
#
# That does not depend on whether the collection has already been loaded
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 7708228d23..2e818dca5d 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -6,8 +6,6 @@ module ActiveRecord
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociation
- alias_method :new, :build
-
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
# loaded and calling collection.size if it has. If it's more likely than not that the collection does
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
@@ -33,9 +31,16 @@ module ActiveRecord
super
end
- def insert_record(record, validate = true)
+ def insert_record(record, validate = true, raise = false)
ensure_not_nested
- return if record.new_record? && !record.save(:validate => validate)
+
+ if record.new_record?
+ if raise
+ record.save!(:validate => validate)
+ else
+ return unless record.save(:validate => validate)
+ end
+ end
through_record(record).save!
update_counter(1)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 7134dc85c8..2131edbc20 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -10,16 +10,16 @@ module ActiveRecord
reflection.klass.transaction do
if target && target != record
- remove_target!(options[:dependent])
+ remove_target!(options[:dependent]) unless target.destroyed?
end
if record
- set_inverse_instance(record)
set_owner_attributes(record)
+ set_inverse_instance(record)
if owner.persisted? && save && !record.save
nullify_owner_attributes(record)
- set_owner_attributes(target)
+ set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 504f25271c..6c878f0f00 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -188,13 +188,12 @@ module ActiveRecord
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
- return if row[join_part.aliased_primary_key].nil?
- association = join_part.instantiate(row)
+ association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
case macro
when :has_many, :has_and_belongs_to_many
other = record.association(join_part.reflection.name)
other.loaded!
- other.target.push(association)
+ other.target.push(association) if association
other.set_inverse_instance(association)
when :belongs_to
set_target_and_inverse(join_part, association, record)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index c32753782f..03963ab060 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -62,6 +62,7 @@ module ActiveRecord
def join_to(relation)
tables = @tables.dup
foreign_table = parent_table
+ foreign_klass = parent.active_record
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
@@ -91,14 +92,17 @@ module ActiveRecord
constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
- unless conditions[i].empty?
- constraint = constraint.and(sanitize(conditions[i], table))
+ conditions = self.conditions[i].dup
+ conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
+
+ unless conditions.empty?
+ constraint = constraint.and(sanitize(conditions, table))
end
relation.from(join(table, constraint))
# The current table in this iteration becomes the foreign table in the next
- foreign_table = table
+ foreign_table, foreign_klass = table, reflection.klass
end
relation
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index eae546e76e..f83138195c 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -32,8 +32,7 @@ module ActiveRecord
end
def table_alias_for(reflection, join = false)
- name = alias_tracker.pluralize(reflection.name)
- name << "_#{alias_suffix}"
+ name = "#{reflection.plural_name}_#{alias_suffix}"
name << "_join" if join
name
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 7256dd5288..779f8164cc 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -68,7 +68,8 @@ module ActiveRecord
private
def associated_records_by_owner
- owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq
+ owners_map = owners_by_key
+ owner_keys = owners_map.keys.compact
if klass.nil? || owner_keys.empty?
records = []
@@ -84,7 +85,7 @@ module ActiveRecord
records.each do |record|
owner_key = record[association_key_name].to_s
- owners_by_key[owner_key].each do |owner|
+ owners_map[owner_key].each do |owner|
records_by_owner[owner] << record
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
index 24be279449..b77b667219 100644
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -13,7 +13,7 @@ module ActiveRecord
# access the aliased column on the join table
def records_for(ids)
scope = super
- klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
+ klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
end
def owner_key_name
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index ea4d73d414..a1a921bcb4 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -17,20 +17,27 @@ module ActiveRecord
replace(record)
end
- def create(attributes = {}, options = {})
- new_record(:create, attributes, options)
+ def create(attributes = {}, options = {}, &block)
+ create_record(attributes, options, &block)
end
- def create!(attributes = {}, options = {})
- build(attributes, options).tap { |record| record.save! }
+ def create!(attributes = {}, options = {}, &block)
+ create_record(attributes, options, true, &block)
end
def build(attributes = {}, options = {})
- new_record(:build, attributes, options)
+ record = build_record(attributes, options)
+ yield(record) if block_given?
+ set_new_record(record)
+ record
end
private
+ def create_scope
+ scoped.scope_for_create.stringify_keys.except(klass.primary_key)
+ end
+
def find_target
scoped.first.tap { |record| set_inverse_instance(record) }
end
@@ -44,10 +51,12 @@ module ActiveRecord
replace(record)
end
- def new_record(method, attributes, options)
- attributes = scoped.scope_for_create.merge(attributes || {})
- record = reflection.send("#{method}_association", attributes, options)
+ def create_record(attributes, options, raise_error = false)
+ record = build_record(attributes, options)
+ yield(record) if block_given?
+ saved = record.save
set_new_record(record)
+ raise RecordInvalid.new(record) if !saved && raise_error
record
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index e436fef46d..b347a94978 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -14,9 +14,10 @@ module ActiveRecord
def target_scope
scope = super
chain[1..-1].each do |reflection|
- # Discard the create with value, as we don't want that the affect the objects we
- # create on the association
- scope = scope.merge(reflection.klass.scoped.create_with(nil))
+ scope = scope.merge(
+ reflection.klass.scoped.with_default_scope.
+ except(:select, :create_with, :includes, :preload, :joins, :eager_load)
+ )
end
scope
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 5833c65893..d7bfaa5655 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/enumerable'
+require 'active_support/deprecation'
module ActiveRecord
# = Active Record Attribute Methods
@@ -11,57 +12,84 @@ module ActiveRecord
# accessors, mutators and query methods.
def define_attribute_methods
return if attribute_methods_generated?
- super(column_names)
- @attribute_methods_generated = true
+
+ if base_class == self
+ super(column_names)
+ @attribute_methods_generated = true
+ else
+ base_class.define_attribute_methods
+ end
end
def attribute_methods_generated?
- @attribute_methods_generated ||= false
+ if base_class == self
+ @attribute_methods_generated ||= false
+ else
+ base_class.attribute_methods_generated?
+ end
end
def undefine_attribute_methods(*args)
- super
- @attribute_methods_generated = false
+ if base_class == self
+ super
+ @attribute_methods_generated = false
+ else
+ base_class.undefine_attribute_methods(*args)
+ end
end
- # Checks whether the method is defined in the model or any of its subclasses
- # that also derive from Active Record. Raises DangerousAttributeError if the
- # method is defined by Active Record though.
def instance_method_already_implemented?(method_name)
- method_name = method_name.to_s
- index = ancestors.index(ActiveRecord::Base) || ancestors.length
- @_defined_class_methods ||= ancestors.first(index).map { |m|
- m.instance_methods(false) | m.private_instance_methods(false)
- }.flatten.map {|m| m.to_s }.to_set
+ if dangerous_attribute_method?(method_name)
+ raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
+ end
- @@_defined_activerecord_methods ||= defined_activerecord_methods
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
- @_defined_class_methods.include?(method_name)
+ super
end
- def defined_activerecord_methods
+ # A method name is 'dangerous' if it is already defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
+ def dangerous_attribute_method?(method_name)
active_record = ActiveRecord::Base
- super_klass = ActiveRecord::Base.superclass
- methods = (active_record.instance_methods - super_klass.instance_methods) +
- (active_record.private_instance_methods - super_klass.private_instance_methods)
- methods.map {|m| m.to_s }.to_set
+ superclass = ActiveRecord::Base.superclass
+
+ (active_record.method_defined?(method_name) ||
+ active_record.private_method_defined?(method_name)) &&
+ !superclass.method_defined?(method_name) &&
+ !superclass.private_method_defined?(method_name)
end
end
- def method_missing(method_id, *args, &block)
- # If we haven't generated any methods yet, generate them, then
- # see if we've created the method we're looking for.
- if !self.class.attribute_methods_generated?
+ # If we haven't generated any methods yet, generate them, then
+ # see if we've created the method we're looking for.
+ def method_missing(method, *args, &block)
+ unless self.class.attribute_methods_generated?
self.class.define_attribute_methods
- method_name = method_id.to_s
- guard_private_attribute_method!(method_name, args)
- send(method_id, *args, &block)
+
+ if respond_to_without_attributes?(method)
+ send(method, *args, &block)
+ else
+ super
+ end
else
super
end
end
- def respond_to?(*args)
+ def attribute_missing(match, *args, &block)
+ if self.class.columns_hash[match.attr_name]
+ ActiveSupport::Deprecation.warn(
+ "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
+ "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
+ "is a column of the table. If this error has happened through normal usage of Active " \
+ "Record (rather than through your own code or external libraries), please report it as " \
+ "a bug."
+ )
+ end
+
+ super
+ end
+
+ def respond_to?(name, include_private = false)
self.class.define_attribute_methods unless self.class.attribute_methods_generated?
super
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 5f06452247..a404a5edd7 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -3,8 +3,7 @@ module ActiveRecord
module PrimaryKey
extend ActiveSupport::Concern
- # Returns this record's primary key value wrapped in an Array or nil if
- # the record is not persisted? or has just been destroyed.
+ # Returns this record's primary key value wrapped in an Array if one is available
def to_key
key = send(self.class.primary_key)
[key] if key
@@ -48,7 +47,7 @@ module ActiveRecord
end
attr_accessor :original_primary_key
-
+
# Attribute writer for the primary key column
def primary_key=(value)
@quoted_primary_key = nil
@@ -67,7 +66,6 @@ module ActiveRecord
@primary_key ||= ''
self.original_primary_key = @primary_key
value &&= value.to_s
- connection_pool.primary_keys[table_name] = value
self.primary_key = block_given? ? instance_eval(&block) : value
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index aef99e3129..4174e4da09 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -6,8 +6,6 @@ module ActiveRecord
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
included do
- attribute_method_suffix ""
-
cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
@@ -58,7 +56,7 @@ module ActiveRecord
generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
end
- # Define an attribute reader method. Cope with nil column.
+ # Define an attribute reader method. Cope with nil column.
# method_name is the same as attr_name except when a non-standard primary key is used,
# we still define #id as an accessor for the key
def define_read_method(method_name, attr_name, column)
@@ -99,8 +97,9 @@ module ActiveRecord
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
- if respond_to? "_#{attr_name}"
- send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
+ method = "_#{attr_name}"
+ if respond_to? method
+ send method if @attributes.has_key?(attr_name.to_s)
else
_read_attribute attr_name
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index c77a3ac145..e9cdb130db 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -17,6 +17,10 @@ module ActiveRecord
write_attribute(attr_name, new_value)
end
end
+
+ if attr_name == primary_key && attr_name != "id"
+ generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='")
+ end
end
end
@@ -24,12 +28,16 @@ module ActiveRecord
# for fixnum and float columns are turned into +nil+.
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
@attributes_cache.delete(attr_name)
- if (column = column_for_attribute(attr_name)) && column.number?
+ column = column_for_attribute(attr_name)
+
+ if column && column.number?
@attributes[attr_name] = convert_number_column_value(value)
- else
+ elsif column || @attributes.has_key?(attr_name)
@attributes[attr_name] = value
+ else
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
end
end
alias_method :raw_write_attribute, :write_attribute
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 48dbe0838a..056170d82a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -161,7 +161,7 @@ module ActiveRecord
#
# For performance reasons, we don't check whether to validate at runtime.
# However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
+ # get created when they are invoked for the very first time. However,
# this can change, for instance, when using nested attributes, which is
# called _after_ the association has been defined. Since we don't want
# the callbacks to get defined multiple times, there are guards that
@@ -347,7 +347,7 @@ module ActiveRecord
end
# reconstruct the scope now that we know the owner's id
- association.send(:construct_scope) if association.respond_to?(:construct_scope)
+ association.send(:reset_scope) if association.respond_to?(:reset_scope)
end
end
@@ -370,7 +370,10 @@ module ActiveRecord
else
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- record[reflection.foreign_key] = key
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
saved = record.save(:validate => !autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index d07f9365b3..78159d13d4 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -177,6 +177,10 @@ module ActiveRecord #:nodoc:
# And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
# <tt>Person.find_all_by_last_name(last_name)</tt>.
#
+ # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
+ # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
+ # like <tt>Person.find_by_last_name!</tt>.
+ #
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
# Person.where(:user_name => user_name, :password => password).first
@@ -393,8 +397,8 @@ module ActiveRecord #:nodoc:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
- cattr_accessor :pluralize_table_names, :instance_writer => false
- @@pluralize_table_names = true
+ class_attribute :pluralize_table_names, :instance_writer => false
+ self.pluralize_table_names = true
##
# :singleton-method:
@@ -438,6 +442,7 @@ module ActiveRecord #:nodoc:
class << self # Class methods
delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
@@ -504,8 +509,7 @@ module ActiveRecord #:nodoc:
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
- object = new(attributes, options)
- yield(object) if block_given?
+ object = new(attributes, options, &block)
object.save
object
end
@@ -577,15 +581,25 @@ module ActiveRecord #:nodoc:
#
# ==== Examples
#
- # class Invoice < ActiveRecord::Base; end;
+ # class Invoice < ActiveRecord::Base
+ # end
+ #
# file class table_name
# invoice.rb Invoice invoices
#
- # class Invoice < ActiveRecord::Base; class Lineitem < ActiveRecord::Base; end; end;
+ # class Invoice < ActiveRecord::Base
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
# file class table_name
# invoice.rb Invoice::Lineitem invoice_lineitems
#
- # module Invoice; class Lineitem < ActiveRecord::Base; end; end;
+ # module Invoice
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
# file class table_name
# invoice/lineitem.rb Invoice::Lineitem lineitems
#
@@ -611,6 +625,8 @@ module ActiveRecord #:nodoc:
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
+ return if abstract_class?
+
self.table_name = compute_table_name
end
@@ -645,8 +661,8 @@ module ActiveRecord #:nodoc:
def set_table_name(value = nil, &block)
@quoted_table_name = nil
define_attr_method :table_name, value, &block
+ @arel_table = nil
- @arel_table = Arel::Table.new(table_name, arel_engine)
@relation = Relation.new(self, arel_table)
end
alias :table_name= :set_table_name
@@ -691,6 +707,10 @@ module ActiveRecord #:nodoc:
# Returns an array of column objects for the table associated with this class.
def columns
+ if defined?(@primary_key)
+ connection_pool.primary_keys[table_name] ||= primary_key
+ end
+
connection_pool.columns[table_name]
end
@@ -699,6 +719,12 @@ module ActiveRecord #:nodoc:
connection_pool.columns_hash[table_name]
end
+ # Returns a hash where the keys are column names and the values are
+ # default values when instantiating the AR object for this table.
+ def column_defaults
+ connection_pool.column_defaults[table_name]
+ end
+
# Returns an array of column names as strings.
def column_names
@column_names ||= columns.map { |column| column.name }
@@ -767,6 +793,17 @@ module ActiveRecord #:nodoc:
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
+ # Returns an array of column names as strings if it's not
+ # an abstract class and table exists.
+ # Otherwise it returns an empty array.
+ def attribute_names
+ @attribute_names ||= if !abstract_class? && table_exists?
+ column_names
+ else
+ []
+ end
+ end
+
# Set the lookup ancestors for ActiveModel.
def lookup_ancestors #:nodoc:
klass = self
@@ -867,7 +904,7 @@ module ActiveRecord #:nodoc:
end
def arel_table
- Arel::Table.new(table_name, arel_engine)
+ @arel_table ||= Arel::Table.new(table_name, arel_engine)
end
def arel_engine
@@ -895,7 +932,7 @@ module ActiveRecord #:nodoc:
# not use the default_scope:
#
# Post.unscoped {
- # limit(10) # Fires "SELECT * FROM posts LIMIT 10"
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
# It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
@@ -911,17 +948,6 @@ module ActiveRecord #:nodoc:
self.current_scope = nil
end
- # Specifies how the record is loaded by +Marshal+.
- #
- # +_load+ sets an instance variable for each key in the hash it takes as input.
- # Override this method if you require more complex marshalling.
- def _load(data)
- record = allocate
- record.init_with(Marshal.load(data))
- record
- end
-
-
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
@@ -1025,20 +1051,15 @@ module ActiveRecord #:nodoc:
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
# is first invoked, so that future attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
- if match = DynamicFinderMatch.match(method_id)
+ if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
- if match.finder?
- options = arguments.extract_options!
- relation = options.any? ? scoped(options) : scoped
- relation.send :find_by_attributes, match, attribute_names, *arguments
- elsif match.instantiator?
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
+ if arguments.size < attribute_names.size
+ method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
+ backtrace = [method_trace] + caller
+ raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
end
- elsif match = DynamicScopeMatch.match(method_id)
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- if match.scope?
+ if match.respond_to?(:scope?) && match.scope?
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
@@ -1047,6 +1068,12 @@ module ActiveRecord #:nodoc:
end # end
METHOD
send(method_id, *arguments)
+ elsif match.finder?
+ options = arguments.extract_options!
+ relation = options.any? ? scoped(options) : scoped
+ relation.send :find_by_attributes, match, attribute_names, *arguments, &block
+ elsif match.instantiator?
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
end
else
super
@@ -1183,11 +1210,11 @@ MSG
end
def current_scope #:nodoc:
- Thread.current[:"#{self}_current_scope"]
+ Thread.current["#{self}_current_scope"]
end
def current_scope=(scope) #:nodoc:
- Thread.current[:"#{self}_current_scope"] = scope
+ Thread.current["#{self}_current_scope"] = scope
end
# Use this macro in your model to set a default scope for all operations on
@@ -1242,22 +1269,43 @@ MSG
def build_default_scope #:nodoc:
if method(:default_scope).owner != Base.singleton_class
- # Use relation.scoping to ensure we ignore whatever the current value of
- # self.current_scope may be.
- relation.scoping { default_scope }
+ evaluate_default_scope { default_scope }
elsif default_scopes.any?
- default_scopes.inject(relation) do |default_scope, scope|
- if scope.is_a?(Hash)
- default_scope.apply_finder_options(scope)
- elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(scope.call)
- else
- default_scope.merge(scope)
+ evaluate_default_scope do
+ default_scopes.inject(relation) do |default_scope, scope|
+ if scope.is_a?(Hash)
+ default_scope.apply_finder_options(scope)
+ elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
+ default_scope.merge(scope.call)
+ else
+ default_scope.merge(scope)
+ end
end
end
end
end
+ def ignore_default_scope? #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"]
+ end
+
+ def ignore_default_scope=(ignore) #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"] = ignore
+ end
+
+ # The ignore_default_scope flag is used to prevent an infinite recursion situation where
+ # a default scope references a scope which has a default scope which references a scope...
+ def evaluate_default_scope
+ return if ignore_default_scope?
+
+ begin
+ self.ignore_default_scope = true
+ yield
+ ensure
+ self.ignore_default_scope = false
+ end
+ end
+
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
@@ -1278,7 +1326,6 @@ MSG
rescue NameError => e
# We don't want to swallow NoMethodError < NameError errors
raise e unless e.instance_of?(NameError)
- rescue ArgumentError
end
end
@@ -1289,7 +1336,7 @@ MSG
# Returns the class descending directly from ActiveRecord::Base or an
# abstract class, if any, in the inheritance hierarchy.
def class_of_active_record_descendant(klass)
- if klass.superclass == Base || klass.superclass.abstract_class?
+ if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
klass
elsif klass.superclass.nil?
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
@@ -1378,9 +1425,8 @@ MSG
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name).alias(default_table_name)
- viz = Arel::Visitors.for(arel_engine)
PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
- viz.accept b
+ connection.visitor.accept b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -1502,6 +1548,7 @@ MSG
@marked_for_destruction = false
@previously_changed = {}
@changed_attributes = {}
+ @relation = nil
ensure_proper_type
set_serialized_attributes
@@ -1510,9 +1557,8 @@ MSG
assign_attributes(attributes, options) if attributes
- result = yield self if block_given?
+ yield self if block_given?
run_callbacks :initialize
- result
end
# Populate +coder+ with attributes about this record that should be
@@ -1543,6 +1589,7 @@ MSG
# post.title # => 'hello world'
def init_with(coder)
@attributes = coder['attributes']
+ @relation = nil
set_serialized_attributes
@@ -1557,16 +1604,6 @@ MSG
self
end
- # Specifies how the record is dumped by +Marshal+.
- #
- # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this
- # method if you require more complex marshalling.
- def _dump(level)
- dump = {}
- encode_with(dump)
- Marshal.dump(dump)
- end
-
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -1606,7 +1643,8 @@ MSG
when new_record?
"#{self.class.model_name.cache_key}/new"
when timestamp = self[:updated_at]
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp.to_s(:number)}"
+ timestamp = timestamp.utc.to_s(:number)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
else
"#{self.class.model_name.cache_key}/#{id}"
end
@@ -1632,9 +1670,6 @@ MSG
# If any attributes are protected by either +attr_protected+ or
# +attr_accessible+ then only settable attributes will be assigned.
#
- # The +guard_protected_attributes+ argument is now deprecated, use
- # the +assign_attributes+ method if you want to bypass mass-assignment security.
- #
# class User < ActiveRecord::Base
# attr_protected :is_admin
# end
@@ -1643,21 +1678,10 @@ MSG
# user.attributes = { :username => 'Phusion', :is_admin => true }
# user.username # => "Phusion"
# user.is_admin? # => false
- def attributes=(new_attributes, guard_protected_attributes = nil)
- unless guard_protected_attributes.nil?
- message = "the use of 'guard_protected_attributes' will be removed from the next major release of rails, " +
- "if you want to bypass mass-assignment security then look into using assign_attributes"
- ActiveSupport::Deprecation.warn(message)
- end
-
+ def attributes=(new_attributes)
return unless new_attributes.is_a?(Hash)
- guard_protected_attributes ||= true
- if guard_protected_attributes
- assign_attributes(new_attributes)
- else
- assign_attributes(new_attributes, :without_protection => true)
- end
+ assign_attributes(new_attributes)
end
# Allows you to set all the attributes for a particular mass-assignment
@@ -1691,12 +1715,11 @@ MSG
return unless new_attributes
attributes = new_attributes.stringify_keys
- role = options[:as] || :default
-
multi_parameter_attributes = []
+ @mass_assignment_options = options
unless options[:without_protection]
- attributes = sanitize_for_mass_assignment(attributes, role)
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
end
attributes.each do |k, v|
@@ -1709,6 +1732,7 @@ MSG
end
end
+ @mass_assignment_options = nil
assign_multiparameter_attributes(multi_parameter_attributes)
end
@@ -1718,7 +1742,7 @@ MSG
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are elided after 50
+ # attribute +attr_name+. String attributes are truncated upto 50
# characters, and Date and Time attributes are returned in the
# <tt>:db</tt> format. Other attributes return the value of
# <tt>#inspect</tt> without modification.
@@ -1763,16 +1787,12 @@ MSG
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
- comparison_object.equal?(self) ||
+ super ||
comparison_object.instance_of?(self.class) &&
id.present? &&
comparison_object.id == id
end
-
- # Delegates to ==
- def eql?(comparison_object)
- self == comparison_object
- end
+ alias :eql? :==
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
@@ -1790,6 +1810,15 @@ MSG
@attributes.frozen?
end
+ # Allows sort on objects
+ def <=>(other_object)
+ if other_object.is_a?(self.class)
+ self.to_key <=> other_object.to_key
+ else
+ nil
+ end
+ end
+
# Backport dup from 1.9 so that initialize_dup() gets called
unless Object.respond_to?(:initialize_dup)
def dup # :nodoc:
@@ -1825,7 +1854,7 @@ MSG
ensure_proper_type
populate_with_current_scope_attributes
- clear_timestamp_attributes
+ super
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
@@ -1841,12 +1870,16 @@ MSG
# Returns the contents of the record as a nicely formatted string.
def inspect
- attributes_as_nice_string = self.class.column_names.collect { |name|
- if has_attribute?(name)
- "#{name}: #{attribute_for_inspect(name)}"
- end
- }.compact.join(", ")
- "#<#{self.class} #{attributes_as_nice_string}>"
+ inspection = if @attributes
+ self.class.column_names.collect { |name|
+ if has_attribute?(name)
+ "#{name}: #{attribute_for_inspect(name)}"
+ end
+ }.compact.join(", ")
+ else
+ "not initialized"
+ end
+ "#<#{self.class} #{inspection}>"
end
protected
@@ -1864,12 +1897,33 @@ MSG
value
end
+ def mass_assignment_options
+ @mass_assignment_options ||= {}
+ end
+
+ def mass_assignment_role
+ mass_assignment_options[:as] || :default
+ end
+
private
+ # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
+ # of the array, and then rescues from the possible NoMethodError. If those elements are
+ # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
+ # which significantly impacts upon performance.
+ #
+ # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
+ #
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
+ def to_ary # :nodoc:
+ nil
+ end
+
def set_serialized_attributes
- (@attributes.keys & self.class.serialized_attributes.keys).each do |key|
- coder = self.class.serialized_attributes[key]
- @attributes[key] = coder.load @attributes[key]
+ sattrs = self.class.serialized_attributes
+
+ sattrs.each do |key, coder|
+ @attributes[key] = coder.load @attributes[key] if @attributes.key?(key)
end
end
@@ -1879,8 +1933,9 @@ MSG
# do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
# No such attribute would be set for objects of the Message class in that example.
def ensure_proper_type
- unless self.class.descends_from_active_record?
- write_attribute(self.class.inheritance_column, self.class.sti_name)
+ klass = self.class
+ if klass.finder_needs_type_condition?
+ write_attribute(klass.inheritance_column, klass.sti_name)
end
end
@@ -1976,18 +2031,21 @@ MSG
# If Date bits were not provided, error
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
+ # If Date bits were provided but blank, then return nil
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+
set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
- # If Date bits were provided but blank, then default to 1
# If Time bits are not there, then default to 0
- [1,1,1,0,0,0].each_with_index{|v,i| set_values[i] = set_values[i].blank? ? v : set_values[i]}
+ (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
instantiate_time_object(name, set_values)
end
def read_date_parameter_value(name, values_hash_from_param)
- set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]}
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+ set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
begin
Date.new(*set_values)
- rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end
@@ -2008,7 +2066,7 @@ MSG
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }
- for pair in pairs
+ pairs.each do |pair|
multiparameter_name, value = pair
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
@@ -2054,16 +2112,10 @@ MSG
end
def populate_with_current_scope_attributes
- self.class.scoped.scope_for_create.each do |att,value|
- respond_to?("#{att}=") && send("#{att}=", value)
- end
- end
+ return unless self.class.scope_attributes?
- # Clear attributes and changed_attributes
- def clear_timestamp_attributes
- all_timestamp_attributes_in_model.each do |attribute_name|
- self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ self.class.scope_attributes.each do |att,value|
+ send("#{att}=", value) if respond_to?("#{att}=")
end
end
end
@@ -2110,6 +2162,5 @@ MSG
end
end
-# TODO: Remove this and make it work with LAZY flag
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/abstract/connection_specification'
ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 6f21cea288..77a5fe1efb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -60,6 +60,7 @@ module ActiveRecord
attr_accessor :automatic_reconnect
attr_reader :spec, :connections
attr_reader :columns, :columns_hash, :primary_keys, :tables
+ attr_reader :column_defaults
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -81,10 +82,11 @@ module ActiveRecord
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
- @connections = []
- @checked_out = []
+ @connections = []
+ @checked_out = []
@automatic_reconnect = true
- @tables = {}
+ @tables = {}
+ @visitor = nil
@columns = Hash.new do |h, table_name|
h[table_name] = with_connection do |conn|
@@ -106,6 +108,12 @@ module ActiveRecord
}]
end
+ @column_defaults = Hash.new do |h, table_name|
+ h[table_name] = Hash[columns[table_name].map { |col|
+ [col.name, col.default]
+ }]
+ end
+
@primary_keys = Hash.new do |h, table_name|
h[table_name] = with_connection do |conn|
table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
@@ -119,6 +127,7 @@ module ActiveRecord
with_connection do |conn|
conn.tables.each { |table| @tables[table] = true }
+ @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name)
end
@tables.key? name
@@ -132,6 +141,7 @@ module ActiveRecord
def clear_cache!
@columns.clear
@columns_hash.clear
+ @column_defaults.clear
@tables.clear
end
@@ -139,6 +149,7 @@ module ActiveRecord
def clear_table_cache!(table_name)
@columns.delete table_name
@columns_hash.delete table_name
+ @column_defaults.delete table_name
@primary_keys.delete table_name
end
@@ -165,7 +176,7 @@ module ActiveRecord
checkin conn if conn
end
- # If a connection already exists yield it to the block. If no connection
+ # If a connection already exists yield it to the block. If no connection
# exists checkout a connection, yield it to the block, and checkin the
# connection when finished.
def with_connection
@@ -262,7 +273,7 @@ module ActiveRecord
else
clear_stale_cached_connections!
if @size == @checked_out.size
- raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
+ raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
end
end
@@ -288,12 +299,22 @@ module ActiveRecord
:connected?, :disconnect!, :with => :@connection_mutex
private
+
def new_connection
- ActiveRecord::Base.send(spec.adapter_method, spec.config)
+ connection = ActiveRecord::Base.send(spec.adapter_method, spec.config)
+
+ # TODO: This is a bit icky, and in the long term we may want to change the method
+ # signature for connections. Also, if we switch to have one visitor per
+ # connection (and therefore per thread), we can get rid of the thread-local
+ # variable in Arel::Visitors::ToSql.
+ @visitor ||= connection.class.visitor_for(self)
+ connection.visitor = @visitor
+
+ connection
end
def current_connection_id #:nodoc:
- Thread.current.object_id
+ ActiveRecord::Base.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
@@ -400,7 +421,7 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(klass)
- pool = @connection_pools[klass.name]
+ pool = @connection_pools.delete(klass.name)
return nil unless pool
pool.automatic_reconnect = false
@@ -425,6 +446,14 @@ module ActiveRecord
@testing = testing
end
+ def method_missing(method_sym, *arguments, &block)
+ @body.send(method_sym, *arguments, &block)
+ end
+
+ def respond_to?(method_sym, include_private = false)
+ super || @body.respond_to?(method_sym)
+ end
+
def each(&block)
body.each(&block)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
index bcd3abc08d..3d0f146fed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -46,9 +46,15 @@ module ActiveRecord
# "database" => "path/to/dbfile"
# )
#
+ # Or a URL:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # "postgres://myuser:mypass@localhost/somedatabase"
+ # )
+ #
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
- def self.establish_connection(spec = nil)
+ def self.establish_connection(spec = ENV["DATABASE_URL"])
case spec
when nil
raise AdapterNotSpecified unless defined?(Rails.env)
@@ -58,6 +64,8 @@ module ActiveRecord
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
+ elsif spec.is_a?(String) && hash = connection_url_to_hash(spec)
+ establish_connection(hash)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
@@ -81,6 +89,24 @@ module ActiveRecord
end
end
+ def self.connection_url_to_hash(url) # :nodoc:
+ config = URI.parse url
+ adapter = config.scheme
+ adapter = "postgresql" if adapter == "postgres"
+ spec = { :adapter => adapter,
+ :username => config.user,
+ :password => config.password,
+ :port => config.port,
+ :database => config.path.sub(%r{^/},""),
+ :host => config.host }
+ spec.reject!{ |_,value| !value }
+ if config.query
+ options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ spec.merge!(options)
+ end
+ spec
+ end
+
class << self
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
@@ -89,6 +115,14 @@ module ActiveRecord
retrieve_connection
end
+ def connection_id
+ Thread.current['ActiveRecord::Base.connection_id']
+ end
+
+ def connection_id=(connection_id)
+ Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ end
+
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
@@ -100,7 +134,7 @@ module ActiveRecord
end
def connection_pool
- connection_handler.retrieve_connection_pool(self)
+ connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
end
def retrieve_connection
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index b3eb23bbb3..dc4a53034b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -1,32 +1,39 @@
-require 'active_support/core_ext/module/deprecation'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
+ # Converts an arel AST to SQL
+ def to_sql(arel)
+ if arel.respond_to?(:ast)
+ visitor.accept(arel.ast)
+ else
+ arel
+ end
+ end
+
# Returns an array of record hashes with the column names as keys and
# column values as values.
- def select_all(sql, name = nil, binds = [])
- select(sql, name, binds)
+ def select_all(arel, name = nil, binds = [])
+ select(to_sql(arel), name, binds)
end
# Returns a record hash with the column names as keys and column values
# as values.
- def select_one(sql, name = nil)
- result = select_all(sql, name)
+ def select_one(arel, name = nil)
+ result = select_all(arel, name)
result.first if result
end
# Returns a single value from a record
- def select_value(sql, name = nil)
- if result = select_one(sql, name)
+ def select_value(arel, name = nil)
+ if result = select_one(arel, name)
result.values.first
end
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
- def select_values(sql, name = nil)
- result = select_rows(sql, name)
+ def select_values(arel, name = nil)
+ result = select_rows(to_sql(arel), name)
result.map { |v| v[0] }
end
@@ -42,7 +49,7 @@ module ActiveRecord
undef_method :execute
# Executes +sql+ statement in the context of this connection using
- # +binds+ as the bind substitutes. +name+ is logged along with
+ # +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_query(sql, name = 'SQL', binds = [])
end
@@ -76,20 +83,20 @@ module ActiveRecord
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
- def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
- sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
+ sql, binds = sql_for_insert(to_sql(arel), pk, id_value, sequence_name, binds)
value = exec_insert(sql, name, binds)
id_value || last_inserted_id(value)
end
# Executes the update statement and returns the number of rows affected.
- def update(sql, name = nil, binds = [])
- exec_update(sql, name, binds)
+ def update(arel, name = nil, binds = [])
+ exec_update(to_sql(arel), name, binds)
end
# Executes the delete statement and returns the number of rows affected.
- def delete(sql, name = nil, binds = [])
- exec_delete(sql, name, binds)
+ def delete(arel, name = nil, binds = [])
+ exec_delete(to_sql(arel), name, binds)
end
# Checks whether there is currently no transaction active. This is done
@@ -245,38 +252,13 @@ module ActiveRecord
# done if the transaction block raises an exception or returns false.
def rollback_db_transaction() end
- # Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
- # fragment that has the same semantics as LIMIT and OFFSET.
- #
- # +options+ must be a Hash which contains a +:limit+ option
- # and an +:offset+ option.
- #
- # This method *modifies* the +sql+ parameter.
- #
- # This method is deprecated!! Stop using it!
- #
- # ===== Examples
- # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
- # generates
- # SELECT * FROM suppliers LIMIT 10 OFFSET 50
- def add_limit_offset!(sql, options)
- if limit = options[:limit]
- sql << " LIMIT #{sanitize_limit(limit)}"
- end
- if offset = options[:offset]
- sql << " OFFSET #{offset.to_i}"
- end
- sql
- end
- deprecate :add_limit_offset!
-
def default_sequence_name(table, column)
nil
end
# Set the sequence to the max value of the table's column.
def reset_sequence!(table, column, sequence = nil)
- # Do nothing by default. Implement for PostgreSQL, Oracle, ...
+ # Do nothing by default. Implement for PostgreSQL, Oracle, ...
end
# Inserts the given fixture into the table. Overridden in adapters that require
@@ -308,10 +290,10 @@ module ActiveRecord
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
- # should look like an integer, or a comma-delimited list of integers, or
+ # should look like an integer, or a comma-delimited list of integers, or
# an Arel SQL literal.
#
- # Returns Integer and Arel::Nodes::SqlLiteral limits as is.
+ # Returns Integer and Arel::Nodes::SqlLiteral limits as is.
# Returns the sanitized limit parameter, either as an integer, or as a
# string which contains a comma-delimited list of integers.
def sanitize_limit(limit)
@@ -324,6 +306,16 @@ module ActiveRecord
end
end
+ # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
+ # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
+ # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ def join_to_update(update, select) #:nodoc:
+ subselect = select.clone
+ subselect.projections = [update.key]
+
+ update.where update.key.in(subselect)
+ end
+
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 093c30aa42..27ff13ad89 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -55,9 +55,10 @@ module ActiveRecord
@query_cache.clear
end
- def select_all(sql, name = nil, binds = [])
+ def select_all(arel, name = nil, binds = [])
if @query_cache_enabled
- cache_sql(sql, binds) { super }
+ sql = to_sql(arel)
+ cache_sql(sql, binds) { super(sql, name, binds) }
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 3de850ec9e..f93c7cd74a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -102,10 +102,13 @@ module ActiveRecord
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
- value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
- else
- value
- end.to_s(:db)
+
+ if value.respond_to?(zone_conversion_method)
+ value = value.send(zone_conversion_method)
+ end
+ end
+
+ value.to_s(:db)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 70a8f6bb58..82f564e41d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -328,7 +328,7 @@ module ActiveRecord
end
# Checks to see if a column exists. See SchemaStatements#column_exists?
- def column_exists?(column_name, type = nil, options = nil)
+ def column_exists?(column_name, type = nil, options = {})
@base.column_exists?(@table_name, column_name, type, options)
end
@@ -386,13 +386,13 @@ module ActiveRecord
# Removes the given index from the table.
#
# ===== Examples
- # ====== Remove the suppliers_name_index in the suppliers table
- # t.remove_index :name
- # ====== Remove the index named accounts_branch_id_index in the accounts table
+ # ====== Remove the index_table_name_on_column in the table_name table
+ # t.remove_index :column
+ # ====== Remove the index named index_table_name_on_branch_id in the table_name table
# t.remove_index :column => :branch_id
- # ====== Remove the index named accounts_branch_id_party_id_index in the accounts table
+ # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table
# t.remove_index :column => [:branch_id, :party_id]
- # ====== Remove the index named by_branch_party in the accounts table
+ # ====== Remove the index named by_branch_party in the table_name table
# t.remove_index :name => :by_branch_party
def remove_index(options = {})
@base.remove_index(@table_name, options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 9f9c2c42cb..8e3ba1297e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
# Returns a Hash of mappings from the abstract data types to the native
- # database types. See TableDefinition#column for details on the recognized
+ # database types. See TableDefinition#column for details on the recognized
# abstract data types.
def native_database_types
{}
@@ -78,7 +78,7 @@ module ActiveRecord
# Creates a new table with the name +table_name+. +table_name+ may either
# be a String or a Symbol.
#
- # There are two ways to work with +create_table+. You can use the block
+ # There are two ways to work with +create_table+. You can use the block
# form or the regular form, like this:
#
# === Block form
@@ -161,7 +161,7 @@ module ActiveRecord
yield td if block_given?
if options[:force] && table_exists?(table_name)
- drop_table(table_name, options)
+ drop_table(table_name)
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
@@ -253,7 +253,7 @@ module ActiveRecord
end
# Drops a table from the database.
- def drop_table(table_name, options = {})
+ def drop_table(table_name)
execute "DROP TABLE #{quote_table_name(table_name)}"
end
@@ -299,7 +299,7 @@ module ActiveRecord
raise NotImplementedError, "rename_column is not implemented"
end
- # Adds a new index to the table. +column_name+ can be a single Symbol, or
+ # Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols.
#
# The index will be named after the table and the first column name,
@@ -346,11 +346,11 @@ module ActiveRecord
# Remove the given index from the table.
#
- # Remove the suppliers_name_index in the suppliers table.
- # remove_index :suppliers, :name
- # Remove the index named accounts_branch_id_index in the accounts table.
+ # Remove the index_accounts_on_column in the accounts table.
+ # remove_index :accounts, :column
+ # Remove the index named index_accounts_on_branch_id in the accounts table.
# remove_index :accounts, :column => :branch_id
- # Remove the index named accounts_branch_id_party_id_index in the accounts table.
+ # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table.
# remove_index :accounts, :column => [:branch_id, :party_id]
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
@@ -564,7 +564,7 @@ module ActiveRecord
def columns_for_remove(table_name, *column_names)
column_names = column_names.flatten
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
column_names.map {|column_name| quote_column_name(column_name) }
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 65024d76f8..443e61b527 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -2,21 +2,33 @@ require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
-
-# TODO: Autoload these files
-require 'active_record/connection_adapters/column'
-require 'active_record/connection_adapters/abstract/schema_definitions'
-require 'active_record/connection_adapters/abstract/schema_statements'
-require 'active_record/connection_adapters/abstract/database_statements'
-require 'active_record/connection_adapters/abstract/quoting'
-require 'active_record/connection_adapters/abstract/connection_pool'
-require 'active_record/connection_adapters/abstract/connection_specification'
-require 'active_record/connection_adapters/abstract/query_cache'
-require 'active_record/connection_adapters/abstract/database_limits'
-require 'active_record/result'
+require 'active_support/deprecation'
module ActiveRecord
module ConnectionAdapters # :nodoc:
+ extend ActiveSupport::Autoload
+
+ autoload :Column
+
+ autoload_under 'abstract' do
+ autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
+ autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
+ autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
+ autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions'
+
+ autoload :SchemaStatements
+ autoload :DatabaseStatements
+ autoload :DatabaseLimits
+ autoload :Quoting
+
+ autoload :ConnectionPool
+ autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool'
+ autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool'
+ autoload :ConnectionSpecification
+
+ autoload :QueryCache
+ end
+
# Active Record supports multiple database systems. AbstractAdapter and
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
@@ -38,12 +50,34 @@ module ActiveRecord
define_callbacks :checkout, :checkin
+ attr_accessor :visitor
+
def initialize(connection, logger = nil) #:nodoc:
@active = nil
@connection, @logger = connection, logger
@query_cache_enabled = false
@query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @open_transactions = 0
@instrumenter = ActiveSupport::Notifications.instrumenter
+ @visitor = nil
+ end
+
+ # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface
+ def self.visitor_for(pool) # :nodoc:
+ adapter = pool.spec.config[:adapter]
+
+ if Arel::Visitors::VISITORS[adapter]
+ ActiveSupport::Deprecation.warn(
+ "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \
+ "should define a visitor_for method which returns the appropriate visitor for " \
+ "the database. For example, MysqlAdapter.visitor_for(pool) returns " \
+ "Arel::Visitors::MySQL.new(pool)."
+ )
+
+ Arel::Visitors::VISITORS[adapter].new(pool)
+ else
+ Arel::Visitors::ToSql.new(pool)
+ end
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -177,12 +211,9 @@ module ActiveRecord
@connection
end
- def open_transactions
- @open_transactions ||= 0
- end
+ attr_reader :open_transactions
def increment_open_transactions
- @open_transactions ||= 0
@open_transactions += 1
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
new file mode 100644
index 0000000000..4b7c74e0b8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -0,0 +1,611 @@
+require 'active_support/core_ext/object/blank'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractMysqlAdapter < AbstractAdapter
+ class Column < ConnectionAdapters::Column # :nodoc:
+ def extract_default(default)
+ if sql_type =~ /blob/i || type == :text
+ if default.blank?
+ return null ? nil : ''
+ else
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ elsif missing_default_forged_as_empty_string?(default)
+ nil
+ else
+ super
+ end
+ end
+
+ def has_default?
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
+ super
+ end
+
+ # Must return the relevant concrete adapter
+ def adapter
+ raise NotImplementedError
+ end
+
+ private
+
+ def simplified_type(field_type)
+ return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
+
+ case field_type
+ when /enum/i, /set/i then :string
+ when /year/i then :integer
+ when /bit/i then :binary
+ else
+ super
+ end
+ end
+
+ def extract_limit(sql_type)
+ case sql_type
+ when /blob|text/i
+ case sql_type
+ when /tiny/i
+ 255
+ when /medium/i
+ 16777215
+ when /long/i
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
+ else
+ super # we could return 65535 here, but we leave it undecorated by default
+ end
+ when /^bigint/i; 8
+ when /^int/i; 4
+ when /^mediumint/i; 3
+ when /^smallint/i; 2
+ when /^tinyint/i; 1
+ else
+ super
+ end
+ end
+
+ # MySQL misreports NOT NULL column default when none is given.
+ # We can't detect this for columns which may have a legitimate ''
+ # default (string) but we can for others (integer, datetime, boolean,
+ # and the rest).
+ #
+ # Test whether the column has default '', is not null, and is not
+ # a type allowing default ''.
+ def missing_default_forged_as_empty_string?(default)
+ type != :string && !null && default == ''
+ end
+ end
+
+ ##
+ # :singleton-method:
+ # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
+ # as boolean. If you wish to disable this emulation (which was the default
+ # behavior in versions 0.13.1 and earlier) you can add the following line
+ # to your application.rb file:
+ #
+ # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false
+ class_attribute :emulate_booleans
+ self.emulate_booleans = true
+
+ LOST_CONNECTION_ERROR_MESSAGES = [
+ "Server shutdown in progress",
+ "Broken pipe",
+ "Lost connection to MySQL server during query",
+ "MySQL server has gone away" ]
+
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
+
+ NATIVE_DATABASE_TYPES = {
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :string => { :name => "varchar", :limit => 255 },
+ :text => { :name => "text" },
+ :integer => { :name => "int", :limit => 4 },
+ :float => { :name => "float" },
+ :decimal => { :name => "decimal" },
+ :datetime => { :name => "datetime" },
+ :timestamp => { :name => "datetime" },
+ :time => { :name => "time" },
+ :date => { :name => "date" },
+ :binary => { :name => "blob" },
+ :boolean => { :name => "tinyint", :limit => 1 }
+ }
+
+ # FIXME: Make the first parameter more similar for the two adapters
+ def initialize(connection, logger, connection_options, config)
+ super(connection, logger)
+ @connection_options, @config = connection_options, config
+ @quoted_column_names, @quoted_table_names = {}, {}
+ end
+
+ def self.visitor_for(pool) # :nodoc:
+ Arel::Visitors::MySQL.new(pool)
+ end
+
+ def adapter_name #:nodoc:
+ self.class::ADAPTER_NAME
+ end
+
+ # Returns true, since this connection adapter supports migrations.
+ def supports_migrations?
+ true
+ end
+
+ def supports_primary_key?
+ true
+ end
+
+ # Returns true, since this connection adapter supports savepoints.
+ def supports_savepoints?
+ true
+ end
+
+ def supports_bulk_alter? #:nodoc:
+ true
+ end
+
+ def native_database_types
+ NATIVE_DATABASE_TYPES
+ end
+
+ # HELPER METHODS ===========================================
+
+ # The two drivers have slightly different ways of yielding hashes of results, so
+ # this method must be implemented to provide a uniform interface.
+ def each_hash(result) # :nodoc:
+ raise NotImplementedError
+ end
+
+ # Overridden by the adapters to instantiate their specific Column type.
+ def new_column(field, default, type, null) # :nodoc:
+ Column.new(field, default, type, null)
+ end
+
+ # Must return the Mysql error number from the exception, if the exception has an
+ # error number.
+ def error_number(exception) # :nodoc:
+ raise NotImplementedError
+ end
+
+ # QUOTING ==================================================
+
+ def quote(value, column = nil)
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
+ s = column.class.string_to_binary(value).unpack("H*")[0]
+ "x'#{s}'"
+ elsif value.kind_of?(BigDecimal)
+ value.to_s("F")
+ else
+ super
+ end
+ end
+
+ def quote_column_name(name) #:nodoc:
+ @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
+ end
+
+ def quote_table_name(name) #:nodoc:
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
+ end
+
+ def quoted_true
+ QUOTED_TRUE
+ end
+
+ def quoted_false
+ QUOTED_FALSE
+ end
+
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity(&block) #:nodoc:
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
+
+ begin
+ update("SET FOREIGN_KEY_CHECKS = 0")
+ yield
+ ensure
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
+ end
+ end
+
+ # DATABASE STATEMENTS ======================================
+
+ # Executes the SQL statement in the context of this connection.
+ def execute(sql, name = nil)
+ if name == :skip_logging
+ @connection.query(sql)
+ else
+ log(sql, name) { @connection.query(sql) }
+ end
+ rescue ActiveRecord::StatementInvalid => exception
+ if exception.message.split(":").first =~ /Packets out of order/
+ raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
+ else
+ raise
+ end
+ end
+
+ # MysqlAdapter has to free a result after using it, so we use this method to write
+ # stuff in a abstract way without concerning ourselves about whether it needs to be
+ # explicitly freed or not.
+ def execute_and_free(sql, name = nil) #:nodoc:
+ yield execute(sql, name)
+ end
+
+ def update_sql(sql, name = nil) #:nodoc:
+ super
+ @connection.affected_rows
+ end
+
+ def begin_db_transaction
+ execute "BEGIN"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def commit_db_transaction #:nodoc:
+ execute "COMMIT"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def rollback_db_transaction #:nodoc:
+ execute "ROLLBACK"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def create_savepoint
+ execute("SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def rollback_to_savepoint
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def release_savepoint
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ end
+
+ # In the simple case, MySQL allows us to place JOINs directly into the UPDATE
+ # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
+ # these, we must use a subquery. However, MySQL is too stupid to create a
+ # temporary table for this automatically, so we have to give it some prompting
+ # in the form of a subsubquery. Ugh!
+ def join_to_update(update, select) #:nodoc:
+ if select.limit || select.offset || select.orders.any?
+ subsubselect = select.clone
+ subsubselect.projections = [update.key]
+
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(update.key.name)
+ subselect.from subsubselect.as('__active_record_temp')
+
+ update.where update.key.in(subselect)
+ else
+ update.table select.source
+ update.wheres = select.constraints
+ end
+ end
+
+ # SCHEMA STATEMENTS ========================================
+
+ def structure_dump #:nodoc:
+ if supports_views?
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
+ else
+ sql = "SHOW TABLES"
+ end
+
+ select_all(sql).map do |table|
+ table.delete('Table_type')
+ sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
+ exec_without_stmt(sql).first['Create Table'] + ";\n\n"
+ end.join("")
+ end
+
+ # Drops the database specified on the +name+ attribute
+ # and creates it again using the provided +options+.
+ def recreate_database(name, options = {})
+ drop_database(name)
+ create_database(name, options)
+ end
+
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
+ # Charset defaults to utf8.
+ #
+ # Example:
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
+ # create_database 'matt_development'
+ # create_database 'matt_development', :charset => :big5
+ def create_database(name, options = {})
+ if options[:collation]
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ else
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ end
+ end
+
+ # Drops a MySQL database.
+ #
+ # Example:
+ # drop_database('sebastian_development')
+ def drop_database(name) #:nodoc:
+ execute "DROP DATABASE IF EXISTS `#{name}`"
+ end
+
+ def current_database
+ select_value 'SELECT DATABASE() as db'
+ end
+
+ # Returns the database character set.
+ def charset
+ show_variable 'character_set_database'
+ end
+
+ # Returns the database collation strategy.
+ def collation
+ show_variable 'collation_database'
+ end
+
+ def tables(name = nil, database = nil) #:nodoc:
+ sql = ["SHOW TABLES", database].compact.join(' IN ')
+
+ execute_and_free(sql, 'SCHEMA') do |result|
+ result.collect { |field| field.first }
+ end
+ end
+
+ def table_exists?(name)
+ return true if super
+
+ name = name.to_s
+ schema, table = name.split('.', 2)
+
+ unless table # A table was provided without a schema
+ table = schema
+ schema = nil
+ end
+
+ tables(nil, schema).include? table
+ end
+
+ # Returns an array of indexes for the given table.
+ def indexes(table_name, name = nil) #:nodoc:
+ indexes = []
+ current_index = nil
+ execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
+ each_hash(result) do |row|
+ if current_index != row[:Key_name]
+ next if row[:Key_name] == 'PRIMARY' # skip the primary key
+ current_index = row[:Key_name]
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
+ end
+
+ indexes.last.columns << row[:Column_name]
+ indexes.last.lengths << row[:Sub_part]
+ end
+ end
+
+ indexes
+ end
+
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
+ def columns(table_name, name = nil)#:nodoc:
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
+ execute_and_free(sql, 'SCHEMA') do |result|
+ each_hash(result).map do |field|
+ new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
+ end
+ end
+ end
+
+ def create_table(table_name, options = {}) #:nodoc:
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
+ end
+
+ def bulk_change_table(table_name, operations) #:nodoc:
+ sqls = operations.map do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_sql"
+
+ if respond_to?(method)
+ send(method, table, *arguments)
+ else
+ raise "Unknown method called : #{method}(#{arguments.inspect})"
+ end
+ end.flatten.join(", ")
+
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
+ end
+
+ # Renames a table.
+ #
+ # Example:
+ # rename_table('octopuses', 'octopi')
+ def rename_table(table_name, new_name)
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
+ end
+
+ def change_column_default(table_name, column_name, default)
+ column = column_for(table_name, column_name)
+ change_column table_name, column_name, column.sql_type, :default => default
+ end
+
+ def change_column_null(table_name, column_name, null, default = nil)
+ column = column_for(table_name, column_name)
+
+ unless null || default.nil?
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ end
+
+ change_column table_name, column_name, column.sql_type, :null => null
+ end
+
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
+ end
+
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ end
+
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
+ return super unless type.to_s == 'integer'
+
+ case limit
+ when 1; 'tinyint'
+ when 2; 'smallint'
+ when 3; 'mediumint'
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
+ when 5..8; 'bigint'
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ end
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ end
+
+ # SHOW VARIABLES LIKE 'name'
+ def show_variable(name)
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
+ variables.first['Value'] unless variables.empty?
+ end
+
+ # Returns a table's primary key and belonging sequence.
+ def pk_and_sequence_for(table)
+ execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result|
+ keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] }
+ keys.length == 1 ? [keys.first, nil] : nil
+ end
+ end
+
+ # Returns just a table's primary key
+ def primary_key(table)
+ pk_and_sequence = pk_and_sequence_for(table)
+ pk_and_sequence && pk_and_sequence.first
+ end
+
+ def case_sensitive_modifier(node)
+ Arel::Nodes::Bin.new(node)
+ end
+
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
+ where_sql
+ end
+
+ protected
+
+ def quoted_columns_for_index(column_names, options = {})
+ length = options[:length] if options.is_a?(Hash)
+
+ case length
+ when Hash
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
+ when Fixnum
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
+ else
+ column_names.map {|name| quote_column_name(name) }
+ end
+ end
+
+ def translate_exception(exception, message)
+ case error_number(exception)
+ when 1062
+ RecordNotUnique.new(message, exception)
+ when 1452
+ InvalidForeignKey.new(message, exception)
+ else
+ super
+ end
+ end
+
+ def add_column_sql(table_name, column_name, type, options = {})
+ add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+ add_column_position!(add_column_sql, options)
+ add_column_sql
+ end
+
+ def change_column_sql(table_name, column_name, type, options = {})
+ column = column_for(table_name, column_name)
+
+ unless options_include_default?(options)
+ options[:default] = column.default
+ end
+
+ unless options.has_key?(:null)
+ options[:null] = column.null
+ end
+
+ change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ change_column_sql
+ end
+
+ def rename_column_sql(table_name, column_name, new_column_name)
+ options = {}
+
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
+ options[:default] = column.default
+ options[:null] = column.null
+ else
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
+ end
+
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
+ rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
+ add_column_options!(rename_column_sql, options)
+ rename_column_sql
+ end
+
+ def remove_column_sql(table_name, *column_names)
+ columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ end
+ alias :remove_columns_sql :remove_column
+
+ def add_index_sql(table_name, column_name, options = {})
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ end
+
+ def remove_index_sql(table_name, options = {})
+ index_name = index_name_for_remove(table_name, options)
+ "DROP INDEX #{index_name}"
+ end
+
+ def add_timestamps_sql(table_name)
+ [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ end
+
+ def remove_timestamps_sql(table_name)
+ [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ end
+
+ private
+
+ def supports_views?
+ version[0] >= 5
+ end
+
+ def column_for(table_name, column_name)
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
+ raise "No such column: #{table_name}.#{column_name}"
+ end
+ column
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 4e3d8a096f..a7856539b7 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -1,3 +1,5 @@
+require 'set'
+
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
@@ -189,7 +191,7 @@ module ActiveRecord
def new_time(year, mon, mday, hour, min, sec, microsec)
# Treat 0000-00-00 00:00:00 as nil.
- return nil if year.nil? || year == 0
+ return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index ac2da73a84..8b574518e5 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,6 +1,6 @@
-# encoding: utf-8
+require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.0'
+gem 'mysql2', '~> 0.3.6'
require 'mysql2'
module ActiveRecord
@@ -20,187 +20,51 @@ module ActiveRecord
end
module ConnectionAdapters
- class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
- end
+ class Mysql2Adapter < AbstractMysqlAdapter
- class Mysql2Column < Column
- BOOL = "tinyint(1)"
- def extract_default(default)
- if sql_type =~ /blob/i || type == :text
- if default.blank?
- return null ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ class Column < AbstractMysqlAdapter::Column # :nodoc:
+ def adapter
+ Mysql2Adapter
end
end
- def has_default?
- return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
- super
- end
-
- private
- def simplified_type(field_type)
- return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
-
- case field_type
- when /enum/i, /set/i then :string
- when /year/i then :integer
- when /bit/i then :binary
- else
- super
- end
- end
-
- def extract_limit(sql_type)
- case sql_type
- when /blob|text/i
- case sql_type
- when /tiny/i
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- else
- super
- end
- end
-
- # MySQL misreports NOT NULL column default when none is given.
- # We can't detect this for columns which may have a legitimate ''
- # default (string) but we can for others (integer, datetime, boolean,
- # and the rest).
- #
- # Test whether the column has default '', is not null, and is not
- # a type allowing default ''.
- def missing_default_forged_as_empty_string?(default)
- type != :string && !null && default == ''
- end
- end
-
- class Mysql2Adapter < AbstractAdapter
- cattr_accessor :emulate_booleans
- self.emulate_booleans = true
-
ADAPTER_NAME = 'Mysql2'
- PRIMARY = "PRIMARY"
-
- LOST_CONNECTION_ERROR_MESSAGES = [
- "Server shutdown in progress",
- "Broken pipe",
- "Lost connection to MySQL server during query",
- "MySQL server has gone away" ]
-
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
-
- NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "int", :limit => 4 },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "tinyint", :limit => 1 }
- }
def initialize(connection, logger, connection_options, config)
- super(connection, logger)
- @connection_options, @config = connection_options, config
- @quoted_column_names, @quoted_table_names = {}, {}
+ super
configure_connection
end
- def adapter_name
- ADAPTER_NAME
- end
-
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
- end
-
- def supports_primary_key?
- true
- end
+ # HELPER METHODS ===========================================
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
- def native_database_types
- NATIVE_DATABASE_TYPES
- end
-
- # QUOTING ==================================================
-
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
- "x'#{s}'"
- elsif value.kind_of?(BigDecimal)
- value.to_s("F")
+ def each_hash(result) # :nodoc:
+ if block_given?
+ result.each(:as => :hash, :symbolize_keys => true) do |row|
+ yield row
+ end
else
- super
+ to_enum(:each_hash, result)
end
end
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name}`"
+ def new_column(field, default, type, null) # :nodoc:
+ Column.new(field, default, type, null)
end
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
+ def error_number(exception)
+ exception.error_number if exception.respond_to?(:error_number)
end
+ # QUOTING ==================================================
+
def quote_string(string)
@connection.escape(string)
end
- def quoted_true
- QUOTED_TRUE
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
def substitute_at(column, index)
Arel.sql "\0"
end
- # REFERENTIAL INTEGRITY ====================================
-
- def disable_referential_integrity(&block) #:nodoc:
- old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
-
- begin
- update("SET FOREIGN_KEY_CHECKS = 0")
- yield
- ensure
- update("SET FOREIGN_KEY_CHECKS = #{old}")
- end
- end
-
# CONNECTION MANAGEMENT ====================================
def active?
@@ -213,11 +77,6 @@ module ActiveRecord
connect
end
- # this is set to true in 2.3, but we don't want it to be
- def requires_reloading?
- false
- end
-
# Disconnects from the database if already connected.
# Otherwise, this method does nothing.
def disconnect!
@@ -273,17 +132,22 @@ module ActiveRecord
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
- if name == :skip_logging
- @connection.query(sql)
- else
- log(sql, name) { @connection.query(sql) }
- end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
- else
- raise
- end
+
+ super
+ end
+
+ def exec_query(sql, name = 'SQL', binds = [])
+ result = execute(sql, name)
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ end
+
+ alias exec_without_stmt exec_query
+
+ # Returns an array of record hashes with the column names as keys and
+ # column values as values.
+ def select(sql, name = nil, binds = [])
+ binds = binds.dup
+ exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -312,380 +176,35 @@ module ActiveRecord
@connection.last_id
end
- def update_sql(sql, name = nil)
- super
- @connection.affected_rows
- end
-
- def begin_db_transaction
- execute "BEGIN"
- rescue Exception
- # Transactions aren't supported
- end
-
- def commit_db_transaction
- execute "COMMIT"
- rescue Exception
- # Transactions aren't supported
- end
-
- def rollback_db_transaction
- execute "ROLLBACK"
- rescue Exception
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
- def add_limit_offset!(sql, options)
- limit, offset = options[:limit], options[:offset]
- if limit && offset
- sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
- elsif limit
- sql << " LIMIT #{sanitize_limit(limit)}"
- elsif offset
- sql << " OFFSET #{offset.to_i}"
- end
- sql
- end
- deprecate :add_limit_offset!
-
- # SCHEMA STATEMENTS ========================================
-
- def structure_dump
- if supports_views?
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
- else
- sql = "SHOW TABLES"
- end
-
- select_all(sql).inject("") do |structure, table|
- table.delete('Table_type')
- structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
- end
- end
-
- # Drops the database specified on the +name+ attribute
- # and creates it again using the provided +options+.
- def recreate_database(name, options = {})
- drop_database(name)
- create_database(name, options)
- end
-
- # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
- # Charset defaults to utf8.
- #
- # Example:
- # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
- # create_database 'matt_development'
- # create_database 'matt_development', :charset => :big5
- def create_database(name, options = {})
- if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
- else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
- end
- end
-
- # Drops a MySQL database.
- #
- # Example:
- # drop_database('sebastian_development')
- def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
- end
-
- def current_database
- select_value 'SELECT DATABASE() as db'
- end
-
- # Returns the database character set.
- def charset
- show_variable 'character_set_database'
- end
-
- # Returns the database collation strategy.
- def collation
- show_variable 'collation_database'
- end
-
- def tables(name = nil, database = nil) #:nodoc:
- sql = ["SHOW TABLES", database].compact.join(' IN ')
- execute(sql, 'SCHEMA').collect do |field|
- field.first
- end
- end
-
- def table_exists?(name)
- return true if super
-
- name = name.to_s
- schema, table = name.split('.', 2)
-
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
-
- tables(nil, schema).include? table
- end
-
- def drop_table(table_name, options = {})
- super(table_name, options)
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- indexes = []
- current_index = nil
- result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA')
- result.each(:symbolize_keys => true, :as => :hash) do |row|
- if current_index != row[:Key_name]
- next if row[:Key_name] == PRIMARY # skip the primary key
- current_index = row[:Key_name]
- indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
- end
-
- indexes.last.columns << row[:Column_name]
- indexes.last.lengths << row[:Sub_part]
- end
- indexes
- end
-
- # Returns an array of +Mysql2Column+ objects for the table specified by +table_name+.
- def columns(table_name, name = nil)
- sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
- columns = []
- result = execute(sql, 'SCHEMA')
- result.each(:symbolize_keys => true, :as => :hash) { |field|
- columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
- }
- columns
- end
-
- def create_table(table_name, options = {})
- super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
- end
-
- # Renames a table.
- #
- # Example:
- # rename_table('octopuses', 'octopi')
- def rename_table(table_name, new_name)
- execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
- end
-
- def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- execute(add_column_sql)
- end
-
- def change_column_default(table_name, column_name, default)
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, :default => default
- end
-
- def change_column_null(table_name, column_name, null, default = nil)
- column = column_for(table_name, column_name)
-
- unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
-
- change_column table_name, column_name, column.sql_type, :null => null
- end
-
- def change_column(table_name, column_name, type, options = {})
- column = column_for(table_name, column_name)
-
- unless options_include_default?(options)
- options[:default] = column.default
- end
-
- unless options.has_key?(:null)
- options[:null] = column.null
- end
-
- change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- execute(change_column_sql)
- end
-
- def rename_column(table_name, column_name, new_column_name)
- options = {}
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
- rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- execute(rename_column_sql)
- end
-
- # Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- return super unless type.to_s == 'integer'
-
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
- end
- end
+ private
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
+ def connect
+ @connection = Mysql2::Client.new(@config)
+ configure_connection
end
- # SHOW VARIABLES LIKE 'name'.
- def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'")
- variables.first['Value'] unless variables.empty?
- end
+ def configure_connection
+ @connection.query_options.merge!(:as => :array)
- # Returns a table's primary key and belonging sequence.
- def pk_and_sequence_for(table)
- keys = []
- result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA')
- result.each(:symbolize_keys => true, :as => :hash) do |row|
- keys << row[:Field] if row[:Key] == "PRI"
- end
- keys.length == 1 ? [keys.first, nil] : nil
- end
+ # By default, MySQL 'where id is null' selects the last inserted id.
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ variable_assignments = ['SQL_AUTO_IS_NULL=0']
+ encoding = @config[:encoding]
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
- end
+ # make sure we set the encoding
+ variable_assignments << "NAMES '#{encoding}'" if encoding
- def case_sensitive_equality_operator
- "= BINARY"
- end
- deprecate :case_sensitive_equality_operator
+ # increase timeout so mysql server doesn't disconnect us
+ wait_timeout = @config[:wait_timeout]
+ wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
+ variable_assignments << "@@wait_timeout = #{wait_timeout}"
- def case_sensitive_modifier(node)
- Arel::Nodes::Bin.new(node)
+ execute("SET #{variable_assignments.join(', ')}", :skip_logging)
end
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- where_sql
+ def version
+ @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
-
- protected
- def quoted_columns_for_index(column_names, options = {})
- length = options[:length] if options.is_a?(Hash)
-
- quoted_column_names = case length
- when Hash
- column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
- when Fixnum
- column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
- else
- column_names.map {|name| quote_column_name(name) }
- end
- end
-
- def translate_exception(exception, message)
- return super unless exception.respond_to?(:error_number)
-
- case exception.error_number
- when 1062
- RecordNotUnique.new(message, exception)
- when 1452
- InvalidForeignKey.new(message, exception)
- else
- super
- end
- end
-
- private
- def connect
- @connection = Mysql2::Client.new(@config)
- configure_connection
- end
-
- def configure_connection
- @connection.query_options.merge!(:as => :array)
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- variable_assignments = ['SQL_AUTO_IS_NULL=0']
- encoding = @config[:encoding]
-
- # make sure we set the encoding
- variable_assignments << "NAMES '#{encoding}'" if encoding
-
- # increase timeout so mysql server doesn't disconnect us
- wait_timeout = @config[:wait_timeout]
- wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
- variable_assignments << "@@wait_timeout = #{wait_timeout}"
-
- execute("SET #{variable_assignments.join(', ')}", :skip_logging)
- end
-
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
- def select(sql, name = nil, binds = [])
- binds = binds.dup
- exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
- end
-
- def exec_query(sql, name = 'SQL', binds = [])
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
-
- log(sql, name, binds) do
- begin
- result = @connection.query(sql)
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
- else
- raise
- end
- end
-
- ActiveRecord::Result.new(result.fields, result.to_a)
- end
- end
-
- def supports_views?
- version[0] >= 5
- end
-
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
- end
-
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index a9f4c08348..a1824fe396 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -1,7 +1,6 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_support/core_ext/kernel/requires'
-require 'active_support/core_ext/object/blank'
-require 'set'
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+require 'active_record/connection_adapters/statement_pool'
+require 'active_support/core_ext/hash/keys'
gem 'mysql', '~> 2.8.1'
require 'mysql'
@@ -41,9 +40,29 @@ module ActiveRecord
end
module ConnectionAdapters
- class MysqlColumn < Column #:nodoc:
- class << self
- def string_to_time(value)
+ # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
+ # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
+ #
+ # Options:
+ #
+ # * <tt>:host</tt> - Defaults to "localhost".
+ # * <tt>:port</tt> - Defaults to 3306.
+ # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
+ # * <tt>:username</tt> - Defaults to "root"
+ # * <tt>:password</tt> - Defaults to nothing.
+ # * <tt>:database</tt> - The name of the database. No default, must be provided.
+ # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
+ # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
+ # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
+ # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
+ # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
+ # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
+ # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
+ #
+ class MysqlAdapter < AbstractMysqlAdapter
+
+ class Column < AbstractMysqlAdapter::Column #:nodoc:
+ def self.string_to_time(value)
return super unless Mysql::Time === value
new_time(
value.year,
@@ -55,230 +74,102 @@ module ActiveRecord
value.second_part)
end
- def string_to_dummy_time(v)
+ def self.string_to_dummy_time(v)
return super unless Mysql::Time === v
new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
end
- def string_to_date(v)
+ def self.string_to_date(v)
return super unless Mysql::Time === v
new_date(v.year, v.month, v.day)
end
- end
- def extract_default(default)
- if sql_type =~ /blob/i || type == :text
- if default.blank?
- return null ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ def adapter
+ MysqlAdapter
end
end
- def has_default?
- return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
- super
- end
+ ADAPTER_NAME = 'MySQL'
- private
- def simplified_type(field_type)
- return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
- return :string if field_type =~ /enum/i
+ class StatementPool < ConnectionAdapters::StatementPool
+ def initialize(connection, max = 1000)
super
+ @cache = Hash.new { |h,pid| h[pid] = {} }
end
- def extract_limit(sql_type)
- case sql_type
- when /blob|text/i
- case sql_type
- when /tiny/i
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- else
- super
+ def each(&block); cache.each(&block); end
+ def key?(key); cache.key?(key); end
+ def [](key); cache[key]; end
+ def length; cache.length; end
+ def delete(key); cache.delete(key); end
+
+ def []=(sql, key)
+ while @max <= cache.size
+ cache.shift.last[:stmt].close
end
+ cache[sql] = key
end
- # MySQL misreports NOT NULL column default when none is given.
- # We can't detect this for columns which may have a legitimate ''
- # default (string) but we can for others (integer, datetime, boolean,
- # and the rest).
- #
- # Test whether the column has default '', is not null, and is not
- # a type allowing default ''.
- def missing_default_forged_as_empty_string?(default)
- type != :string && !null && default == ''
+ def clear
+ cache.values.each do |hash|
+ hash[:stmt].close
+ end
+ cache.clear
end
- end
- # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
- # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
- #
- # Options:
- #
- # * <tt>:host</tt> - Defaults to "localhost".
- # * <tt>:port</tt> - Defaults to 3306.
- # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
- # * <tt>:username</tt> - Defaults to "root"
- # * <tt>:password</tt> - Defaults to nothing.
- # * <tt>:database</tt> - The name of the database. No default, must be provided.
- # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
- # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
- # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
- #
- class MysqlAdapter < AbstractAdapter
-
- ##
- # :singleton-method:
- # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
- # as boolean. If you wish to disable this emulation (which was the default
- # behavior in versions 0.13.1 and earlier) you can add the following line
- # to your application.rb file:
- #
- # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
- cattr_accessor :emulate_booleans
- self.emulate_booleans = true
-
- ADAPTER_NAME = 'MySQL'
-
- LOST_CONNECTION_ERROR_MESSAGES = [
- "Server shutdown in progress",
- "Broken pipe",
- "Lost connection to MySQL server during query",
- "MySQL server has gone away" ]
-
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
-
- NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "int", :limit => 4 },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "tinyint", :limit => 1 }
- }
+ private
+ def cache
+ @cache[$$]
+ end
+ end
def initialize(connection, logger, connection_options, config)
- super(connection, logger)
- @connection_options, @config = connection_options, config
- @quoted_column_names, @quoted_table_names = {}, {}
- @statements = {}
+ super
+ @statements = StatementPool.new(@connection,
+ config.fetch(:statement_limit) { 1000 })
@client_encoding = nil
connect
end
- def adapter_name #:nodoc:
- ADAPTER_NAME
- end
-
- def supports_bulk_alter? #:nodoc:
- true
- end
-
# Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
true
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations? #:nodoc:
- true
- end
+ # HELPER METHODS ===========================================
- # Returns true.
- def supports_primary_key? #:nodoc:
- true
+ def each_hash(result) # :nodoc:
+ if block_given?
+ result.each_hash do |row|
+ row.symbolize_keys!
+ yield row
+ end
+ else
+ to_enum(:each_hash, result)
+ end
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints? #:nodoc:
- true
+ def new_column(field, default, type, null) # :nodoc:
+ Column.new(field, default, type, null)
end
- def native_database_types #:nodoc:
- NATIVE_DATABASE_TYPES
+ def error_number(exception) # :nodoc:
+ exception.errno if exception.respond_to?(:errno)
end
-
# QUOTING ==================================================
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
- "x'#{s}'"
- elsif value.kind_of?(BigDecimal)
- value.to_s("F")
- else
- super
- end
- end
-
def type_cast(value, column)
return super unless value == true || value == false
value ? 1 : 0
end
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name}`"
- end
-
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
- def quoted_true
- QUOTED_TRUE
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
- # REFERENTIAL INTEGRITY ====================================
-
- def disable_referential_integrity #:nodoc:
- old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
-
- begin
- update("SET FOREIGN_KEY_CHECKS = 0")
- yield
- ensure
- update("SET FOREIGN_KEY_CHECKS = #{old}")
- end
- end
-
# CONNECTION MANAGEMENT ====================================
def active?
@@ -330,9 +221,6 @@ module ActiveRecord
# Clears the prepared statements cache.
def clear_cache!
- @statements.values.each do |cache|
- cache[:stmt].close
- end
@statements.clear
end
@@ -407,7 +295,7 @@ module ActiveRecord
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
# Some queries, like SHOW CREATE TABLE don't work through the prepared
- # statement API. For those queries, we need to use this method. :'(
+ # statement API. For those queries, we need to use this method. :'(
log(sql, name) do
result = @connection.query(sql)
cols = []
@@ -422,20 +310,11 @@ module ActiveRecord
end
end
- # Executes an SQL query and returns a MySQL::Result object. Note that you have to free
- # the Result object after you're done using it.
- def execute(sql, name = nil) #:nodoc:
- if name == :skip_logging
- @connection.query(sql)
- else
- log(sql, name) { @connection.query(sql) }
- end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
- else
- raise
- end
+ def execute_and_free(sql, name = nil)
+ result = execute(sql, name)
+ ret = yield result
+ result.free
+ ret
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
@@ -444,11 +323,6 @@ module ActiveRecord
end
alias :create :insert_sql
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.affected_rows
- end
-
def exec_delete(sql, name, binds)
log(sql, name, binds) do
exec_stmt(sql, name, binds) do |cols, stmt|
@@ -464,359 +338,8 @@ module ActiveRecord
# Transactions aren't supported
end
- def commit_db_transaction #:nodoc:
- execute "COMMIT"
- rescue Exception
- # Transactions aren't supported
- end
-
- def rollback_db_transaction #:nodoc:
- execute "ROLLBACK"
- rescue Exception
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
- def add_limit_offset!(sql, options) #:nodoc:
- limit, offset = options[:limit], options[:offset]
- if limit && offset
- sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
- elsif limit
- sql << " LIMIT #{sanitize_limit(limit)}"
- elsif offset
- sql << " OFFSET #{offset.to_i}"
- end
- sql
- end
- deprecate :add_limit_offset!
-
- # SCHEMA STATEMENTS ========================================
-
- def structure_dump #:nodoc:
- if supports_views?
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
- else
- sql = "SHOW TABLES"
- end
-
- select_all(sql).map do |table|
- table.delete('Table_type')
- sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
- exec_without_stmt(sql).first['Create Table'] + ";\n\n"
- end.join("")
- end
-
- # Drops the database specified on the +name+ attribute
- # and creates it again using the provided +options+.
- def recreate_database(name, options = {}) #:nodoc:
- drop_database(name)
- create_database(name, options)
- end
-
- # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
- # Charset defaults to utf8.
- #
- # Example:
- # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
- # create_database 'matt_development'
- # create_database 'matt_development', :charset => :big5
- def create_database(name, options = {})
- if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
- else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
- end
- end
-
- # Drops a MySQL database.
- #
- # Example:
- # drop_database 'sebastian_development'
- def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
- end
-
- def current_database
- select_value 'SELECT DATABASE() as db'
- end
-
- # Returns the database character set.
- def charset
- show_variable 'character_set_database'
- end
-
- # Returns the database collation strategy.
- def collation
- show_variable 'collation_database'
- end
-
- def tables(name = nil, database = nil) #:nodoc:
- result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA')
- tables = result.collect { |field| field[0] }
- result.free
- tables
- end
-
- def table_exists?(name)
- return true if super
-
- name = name.to_s
- schema, table = name.split('.', 2)
-
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
-
- tables(nil, schema).include? table
- end
-
- def drop_table(table_name, options = {})
- super(table_name, options)
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)#:nodoc:
- indexes = []
- current_index = nil
- result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
- result.each do |row|
- if current_index != row[2]
- next if row[2] == "PRIMARY" # skip the primary key
- current_index = row[2]
- indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
- end
-
- indexes.last.columns << row[4]
- indexes.last.lengths << row[7]
- end
- result.free
- indexes
- end
-
- # Returns an array of +MysqlColumn+ objects for the table specified by +table_name+.
- def columns(table_name, name = nil)#:nodoc:
- sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
- result = execute(sql, 'SCHEMA')
- columns = result.collect { |field| MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
- result.free
- columns
- end
-
- def create_table(table_name, options = {}) #:nodoc:
- super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
- end
-
- # Renames a table.
- #
- # Example:
- # rename_table('octopuses', 'octopi')
- def rename_table(table_name, new_name)
- execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
- end
-
- def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.map do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_sql"
-
- if respond_to?(method)
- send(method, table, *arguments)
- else
- raise "Unknown method called : #{method}(#{arguments.inspect})"
- end
- end.flatten.join(", ")
-
- execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
- end
-
- def add_column(table_name, column_name, type, options = {})
- execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
- end
-
- def change_column_default(table_name, column_name, default) #:nodoc:
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, :default => default
- end
-
- def change_column_null(table_name, column_name, null, default = nil)
- column = column_for(table_name, column_name)
-
- unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
-
- change_column table_name, column_name, column.sql_type, :null => null
- end
-
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
- end
-
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
- end
-
- # Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- return super unless type.to_s == 'integer'
-
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
- end
- end
-
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
- end
-
- # SHOW VARIABLES LIKE 'name'
- def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'")
- variables.first['Value'] unless variables.empty?
- end
-
- # Returns a table's primary key and belonging sequence.
- def pk_and_sequence_for(table) #:nodoc:
- keys = []
- result = execute("describe #{quote_table_name(table)}", 'SCHEMA')
- result.each_hash do |h|
- keys << h["Field"]if h["Key"] == "PRI"
- end
- result.free
- keys.length == 1 ? [keys.first, nil] : nil
- end
-
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
- end
-
- def case_sensitive_equality_operator
- "= BINARY"
- end
- deprecate :case_sensitive_equality_operator
-
- def case_sensitive_modifier(node)
- Arel::Nodes::Bin.new(node)
- end
-
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- where_sql
- end
-
- protected
- def quoted_columns_for_index(column_names, options = {})
- length = options[:length] if options.is_a?(Hash)
-
- quoted_column_names = case length
- when Hash
- column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
- when Fixnum
- column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
- else
- column_names.map {|name| quote_column_name(name) }
- end
- end
-
- def translate_exception(exception, message)
- return super unless exception.respond_to?(:errno)
-
- case exception.errno
- when 1062
- RecordNotUnique.new(message, exception)
- when 1452
- InvalidForeignKey.new(message, exception)
- else
- super
- end
- end
-
- def add_column_sql(table_name, column_name, type, options = {})
- add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- add_column_sql
- end
-
- def remove_column_sql(table_name, *column_names)
- columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
- end
- alias :remove_columns_sql :remove_column
-
- def change_column_sql(table_name, column_name, type, options = {})
- column = column_for(table_name, column_name)
-
- unless options_include_default?(options)
- options[:default] = column.default
- end
-
- unless options.has_key?(:null)
- options[:null] = column.null
- end
-
- change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- change_column_sql
- end
-
- def rename_column_sql(table_name, column_name, new_column_name)
- options = {}
-
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
-
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
- rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- rename_column_sql
- end
-
- def add_index_sql(table_name, column_name, options = {})
- index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
- "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
- end
-
- def remove_index_sql(table_name, options = {})
- index_name = index_name_for_remove(table_name, options)
- "DROP INDEX #{index_name}"
- end
-
- def add_timestamps_sql(table_name)
- [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
- end
-
- def remove_timestamps_sql(table_name)
- [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
- end
-
private
+
def exec_stmt(sql, name, binds)
cache = {}
if binds.empty?
@@ -828,12 +351,11 @@ module ActiveRecord
stmt = cache[:stmt]
end
-
begin
stmt.execute(*binds.map { |col, val| type_cast(val, col) })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
+ # place when an error occurs. To support older mysql versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
@@ -857,59 +379,48 @@ module ActiveRecord
result
end
- def connect
- encoding = @config[:encoding]
- if encoding
- @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
- end
+ def connect
+ encoding = @config[:encoding]
+ if encoding
+ @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
+ end
- if @config[:sslca] || @config[:sslkey]
- @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
- end
+ if @config[:sslca] || @config[:sslkey]
+ @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
+ end
- @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
- @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
- @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
+ @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
+ @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
+ @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
- @connection.real_connect(*@connection_options)
+ @connection.real_connect(*@connection_options)
- # reconnect must be set after real_connect is called, because real_connect sets it to false internally
- @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
+ # reconnect must be set after real_connect is called, because real_connect sets it to false internally
+ @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
- configure_connection
- end
-
- def configure_connection
- encoding = @config[:encoding]
- execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
- end
+ configure_connection
+ end
- def select(sql, name = nil, binds = [])
- @connection.query_with_result = true
- rows = exec_query(sql, name, binds).to_a
- @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
- rows
- end
+ def configure_connection
+ encoding = @config[:encoding]
+ execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
- def supports_views?
- version[0] >= 5
- end
+ # By default, MySQL 'where id is null' selects the last inserted id.
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
+ end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
- end
+ def select(sql, name = nil, binds = [])
+ @connection.query_with_result = true
+ rows = exec_query(sql, name, binds).to_a
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
+ rows
+ end
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
- end
+ # Returns the version of the connected MySQL server.
+ def version
+ @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 37db2be7a9..d859843260 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_adapter'
-require 'active_support/core_ext/kernel/requires'
require 'active_support/core_ext/object/blank'
+require 'active_record/connection_adapters/statement_pool'
# Make sure we're using pg high enough for PGResult#values
gem 'pg', '~> 0.11'
@@ -39,6 +39,16 @@ module ActiveRecord
# :stopdoc:
class << self
attr_accessor :money_precision
+ def string_to_time(string)
+ return string unless String === string
+
+ case string
+ when 'infinity' then 1.0 / 0.0
+ when '-infinity' then -1.0 / 0.0
+ else
+ super
+ end
+ end
end
# :startdoc:
@@ -189,7 +199,7 @@ module ActiveRecord
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
- # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
+ # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
# <encoding></tt> call on the connection.
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
@@ -237,6 +247,47 @@ module ActiveRecord
true
end
+ class StatementPool < ConnectionAdapters::StatementPool
+ def initialize(connection, max)
+ super
+ @counter = 0
+ @cache = Hash.new { |h,pid| h[pid] = {} }
+ end
+
+ def each(&block); cache.each(&block); end
+ def key?(key); cache.key?(key); end
+ def [](key); cache[key]; end
+ def length; cache.length; end
+
+ def next_key
+ "a#{@counter + 1}"
+ end
+
+ def []=(sql, key)
+ while @max <= cache.size
+ dealloc(cache.shift.last)
+ end
+ @counter += 1
+ cache[sql] = key
+ end
+
+ def clear
+ cache.each_value do |stmt_key|
+ dealloc stmt_key
+ end
+ cache.clear
+ end
+
+ private
+ def cache
+ @cache[$$]
+ end
+
+ def dealloc(key)
+ @connection.query "DEALLOCATE #{key}"
+ end
+ end
+
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@@ -245,9 +296,10 @@ module ActiveRecord
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
@table_alias_length = nil
- @statements = {}
connect
+ @statements = StatementPool.new @connection,
+ config.fetch(:statement_limit) { 1000 }
if postgresql_version < 80200
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
@@ -256,11 +308,12 @@ module ActiveRecord
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
end
+ def self.visitor_for(pool) # :nodoc:
+ Arel::Visitors::PostgreSQL.new(pool)
+ end
+
# Clears the prepared statements cache.
def clear_cache!
- @statements.each_value do |value|
- @connection.query "DEALLOCATE #{value}"
- end
@statements.clear
end
@@ -349,6 +402,9 @@ module ActiveRecord
return super unless column
case value
+ when Float
+ return super unless value.infinite? && column.type == :datetime
+ "'#{value.to_s.downcase}'"
when Numeric
return super unless column.sql_type == 'money'
# Not truly string input, so doesn't require (or allow) escape string syntax.
@@ -454,10 +510,11 @@ module ActiveRecord
# Executes an INSERT query and returns the new record's ID
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- # Extract the table from the insert sql. Yuck.
- _, table = extract_schema_and_table(sql.split(" ", 4)[2])
-
- pk ||= primary_key(table)
+ unless pk
+ # Extract the table from the insert sql. Yuck.
+ table_ref = extract_table_ref_from_insert_sql(sql)
+ pk = primary_key(table_ref) if table_ref
+ end
if pk
select_value("#{sql} RETURNING #{quote_column_name(pk)}")
@@ -553,9 +610,9 @@ module ActiveRecord
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
unless pk
- _, table = extract_schema_and_table(sql.split(" ", 4)[2])
-
- pk = primary_key(table)
+ # Extract the table from the insert sql. Yuck.
+ table_ref = extract_table_ref_from_insert_sql(sql)
+ pk = primary_key(table_ref) if table_ref
end
sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk
@@ -601,12 +658,14 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def recreate_database(name) #:nodoc:
+ # Drops the database specified on the +name+ attribute
+ # and creates it again using the provided +options+.
+ def recreate_database(name, options = {}) #:nodoc:
drop_database(name)
- create_database(name)
+ create_database(name, options)
end
- # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
# <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -653,34 +712,33 @@ module ActiveRecord
SQL
end
+ # Returns true if table exists.
+ # If the schema is not specified as part of +name+ then it will only find tables within
+ # the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = extract_schema_and_table(name.to_s)
+ schema, table = Utils.extract_schema_and_table(name.to_s)
+ return false unless table
- binds = [[nil, table.gsub(/(^"|"$)/,'')]]
+ binds = [[nil, table]]
binds << [nil, schema] if schema
exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0
SELECT COUNT(*)
- FROM pg_tables
- WHERE tablename = $1
- #{schema ? "AND schemaname = $2" : ''}
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind in ('v','r')
+ AND c.relname = $1
+ AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'}
SQL
end
- # Extracts the table and schema name from +name+
- def extract_schema_and_table(name)
- schema, table = name.split('.', 2)
-
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
-
- if name =~ /^"/ # Handle quoted table names
- table = name
- schema = nil
- end
- [schema, table]
+ # Returns true if schema exists.
+ def schema_exists?(name)
+ exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_namespace
+ WHERE nspname = $1
+ SQL
end
# Returns an array of indexes for the given table.
@@ -688,11 +746,11 @@ module ActiveRecord
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
- FROM pg_class t, pg_class i, pg_index d
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
- AND d.indexrelid = i.oid
AND d.indisprimary = 'f'
- AND t.oid = d.indrelid
AND t.relname = '#{table_name}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
ORDER BY i.relname
@@ -730,6 +788,11 @@ module ActiveRecord
query('select current_database()')[0][0]
end
+ # Returns the current schema name.
+ def current_schema
+ query('SELECT current_schema', 'SCHEMA')[0][0]
+ end
+
# Returns the current database encoding format.
def encoding
query(<<-end_sql)[0][0]
@@ -793,7 +856,7 @@ module ActiveRecord
end
if pk && sequence
- quoted_sequence = quote_column_name(sequence)
+ quoted_sequence = quote_table_name(sequence)
select_value <<-end_sql, 'Reset sequence'
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
@@ -806,24 +869,25 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = exec_query(<<-end_sql, 'SCHEMA').rows.first
- SELECT attr.attname, seq.relname
- FROM pg_class seq,
- pg_attribute attr,
- pg_depend dep,
- pg_namespace name,
- pg_constraint cons
- WHERE seq.oid = dep.objid
- AND seq.relkind = 'S'
- AND attr.attrelid = dep.refobjid
- AND attr.attnum = dep.refobjsubid
- AND attr.attrelid = cons.conrelid
- AND attr.attnum = cons.conkey[1]
- AND cons.contype = 'p'
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
+ SELECT attr.attname, ns.nspname, seq.relname
+ FROM pg_class seq
+ INNER JOIN pg_depend dep ON seq.oid = dep.objid
+ INNER JOIN pg_attribute attr ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ INNER JOIN pg_namespace ns ON seq.relnamespace = ns.oid
+ WHERE seq.relkind = 'S'
+ AND cons.contype = 'p'
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
# [primary_key, sequence]
- [result.first, result.last]
+ if result.second == 'public' then
+ sequence = result.last
+ else
+ sequence = result.second+'.'+result.last
+ end
+
+ [result.first, sequence]
rescue
nil
end
@@ -832,16 +896,11 @@ module ActiveRecord
def primary_key(table)
row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first
SELECT DISTINCT(attr.attname)
- FROM pg_attribute attr,
- pg_depend dep,
- pg_namespace name,
- pg_constraint cons
- WHERE attr.attrelid = dep.refobjid
- AND attr.attnum = dep.refobjsubid
- AND attr.attrelid = cons.conrelid
- AND attr.attnum = cons.conkey[1]
- AND cons.contype = 'p'
- AND dep.refobjid = $1::regclass
+ FROM pg_attribute attr
+ INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ WHERE cons.contype = 'p'
+ AND dep.refobjid = $1::regclass
end_sql
row && row.first
@@ -927,13 +986,32 @@ module ActiveRecord
# Construct a clean list of column names from the ORDER BY clause, removing
# any ASC/DESC modifiers
- order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s }
+ order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') }
order_columns.delete_if { |c| c.blank? }
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
"DISTINCT #{columns}, #{order_columns * ', '}"
end
+ module Utils
+ extend self
+
+ # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
+ # +schema_name+ is nil if not specified in +name+.
+ # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
+ # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
+ #
+ # * <tt>table_name</tt>
+ # * <tt>"table.name"</tt>
+ # * <tt>schema_name.table_name</tt>
+ # * <tt>schema_name."table.name"</tt>
+ # * <tt>"schema.name"."table name"</tt>
+ def extract_schema_and_table(name)
+ table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
+ [schema, table]
+ end
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
def postgresql_version
@@ -952,27 +1030,28 @@ module ActiveRecord
end
private
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
- end
-
- def exec_cache(sql, binds)
- unless @statements.key? sql
- nextkey = "a#{@statements.length + 1}"
- @connection.prepare nextkey, sql
- @statements[sql] = nextkey
+ def exec_no_cache(sql, binds)
+ @connection.async_exec(sql)
end
- key = @statements[sql]
+ def exec_cache(sql, binds)
+ sql_key = "#{schema_search_path}-#{sql}"
+ unless @statements.key? sql_key
+ nextkey = @statements.next_key
+ @connection.prepare nextkey, sql
+ @statements[sql_key] = nextkey
+ end
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- end
+ key = @statements[sql_key]
+
+ # Clear the queue
+ @connection.get_last_result
+ @connection.send_query_prepared(key, binds.map { |col, val|
+ type_cast(val, col)
+ })
+ @connection.block
+ @connection.get_last_result
+ end
# The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
@@ -1072,9 +1151,14 @@ module ActiveRecord
end
end
- def table_definition
- TableDefinition.new(self)
- end
+ def extract_table_ref_from_insert_sql(sql)
+ sql[/into\s+([^\(]*).*values\s*\(/i]
+ $1.strip if $1
+ end
+
+ def table_definition
+ TableDefinition.new(self)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index c3a7b039ff..0a0da0b5d3 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,4 +1,6 @@
require 'active_record/connection_adapters/sqlite_adapter'
+
+gem 'sqlite3', '~> 1.3.4'
require 'sqlite3'
module ActiveRecord
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index d2785b234a..1932a849ee 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -1,5 +1,6 @@
require 'active_record/connection_adapters/abstract_adapter'
-require 'active_support/core_ext/kernel/requires'
+require 'active_record/connection_adapters/statement_pool'
+require 'active_support/core_ext/string/encoding'
module ActiveRecord
module ConnectionAdapters #:nodoc:
@@ -48,12 +49,52 @@ module ActiveRecord
end
end
+ class StatementPool < ConnectionAdapters::StatementPool
+ def initialize(connection, max)
+ super
+ @cache = Hash.new { |h,pid| h[pid] = {} }
+ end
+
+ def each(&block); cache.each(&block); end
+ def key?(key); cache.key?(key); end
+ def [](key); cache[key]; end
+ def length; cache.length; end
+
+ def []=(sql, key)
+ while @max <= cache.size
+ dealloc(cache.shift.last[:stmt])
+ end
+ cache[sql] = key
+ end
+
+ def clear
+ cache.values.each do |hash|
+ dealloc hash[:stmt]
+ end
+ cache.clear
+ end
+
+ private
+ def cache
+ @cache[$$]
+ end
+
+ def dealloc(stmt)
+ stmt.close unless stmt.closed?
+ end
+ end
+
def initialize(connection, logger, config)
super(connection, logger)
- @statements = {}
+ @statements = StatementPool.new(@connection,
+ config.fetch(:statement_limit) { 1000 })
@config = config
end
+ def self.visitor_for(pool) # :nodoc:
+ Arel::Visitors::SQLite.new(pool)
+ end
+
def adapter_name #:nodoc:
'SQLite'
end
@@ -141,7 +182,7 @@ module ActiveRecord
end
def quote_column_name(name) #:nodoc:
- %Q("#{name}")
+ %Q("#{name.to_s.gsub('"', '""')}")
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -154,6 +195,26 @@ module ActiveRecord
end
end
+ if "<3".encoding_aware?
+ def type_cast(value, column) # :nodoc:
+ return value.to_f if BigDecimal === value
+ return super unless String === value
+ return super unless column && value
+
+ value = super
+ if column.type == :string && value.encoding == Encoding::ASCII_8BIT
+ @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
+ value.encode! 'utf-8'
+ end
+ value
+ end
+ else
+ def type_cast(value, column) # :nodoc:
+ return super unless BigDecimal === value
+
+ value.to_f
+ end
+ end
# DATABASE STATEMENTS ======================================
@@ -230,15 +291,15 @@ module ActiveRecord
end
def begin_db_transaction #:nodoc:
- @connection.transaction
+ log('begin transaction',nil) { @connection.transaction }
end
def commit_db_transaction #:nodoc:
- @connection.commit
+ log('commit transaction',nil) { @connection.commit }
end
def rollback_db_transaction #:nodoc:
- @connection.rollback
+ log('rollback transaction',nil) { @connection.rollback }
end
# SCHEMA STATEMENTS ========================================
@@ -320,7 +381,7 @@ module ActiveRecord
end
def remove_column(table_name, *column_names) #:nodoc:
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
column_names.flatten.each do |column_name|
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
new file mode 100644
index 0000000000..c6b1bc8b5b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class StatementPool
+ include Enumerable
+
+ def initialize(connection, max = 1000)
+ @connection = connection
+ @max = max
+ end
+
+ def each
+ raise NotImplementedError
+ end
+
+ def key?(key)
+ raise NotImplementedError
+ end
+
+ def [](key)
+ raise NotImplementedError
+ end
+
+ def length
+ raise NotImplementedError
+ end
+
+ def []=(sql, key)
+ raise NotImplementedError
+ end
+
+ def clear
+ raise NotImplementedError
+ end
+
+ def delete(key)
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 7839f03848..3c7defedac 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -2,7 +2,7 @@ module ActiveRecord
# = Active Record Counter Cache
module CounterCache
# Resets one or more counter caches to their correct value using an SQL
- # count query. This is useful when adding new counter caches, or if the
+ # count query. This is useful when adding new counter caches, or if the
# counter has been corrupted or modified directly by SQL.
#
# ==== Parameters
@@ -33,7 +33,7 @@ module ActiveRecord
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
})
- connection.update stmt.to_sql
+ connection.update stmt
end
return true
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index ea1709cb1f..96870cb338 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -87,7 +87,7 @@ module ActiveRecord
#
# For example, in
#
- # Location.find :all, :conditions => ["lat = ? AND lng = ?", 53.7362]
+ # Location.where("lat = ? AND lng = ?", 53.7362)
#
# two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
@@ -169,4 +169,17 @@ module ActiveRecord
@errors = errors
end
end
+
+ # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ class UnknownPrimaryKey < ActiveRecordError
+ attr_reader :model
+
+ def initialize(model)
+ @model = model
+ end
+
+ def message
+ "Unknown primary key for table #{model.table_name} in model #{model}."
+ end
+ end
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 4aa6389a04..cad9417216 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -6,14 +6,13 @@ rescue LoadError
end
require 'yaml'
-require 'csv'
require 'zlib'
require 'active_support/dependencies'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/logger'
require 'active_support/ordered_hash'
-require 'active_support/core_ext/module/deprecation'
+require 'active_record/fixtures/file'
if defined? ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -25,378 +24,368 @@ end
class FixturesFileNotFound < StandardError; end
-# Fixtures are a way of organizing data that you want to test against; in short, sample data.
-#
-# = Fixture formats
-#
-# Fixtures come in 1 flavor:
-#
-# 1. YAML fixtures
-#
-# == YAML fixtures
-#
-# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
-# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
-#
-# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed
-# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is
-# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
-# The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
-# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this:
-#
-# rubyonrails:
-# id: 1
-# name: Ruby on Rails
-# url: http://www.rubyonrails.org
-#
-# google:
-# id: 2
-# name: Google
-# url: http://www.google.com
-#
-# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an
-# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
-# pleasure.
-#
-# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
-# See http://yaml.org/type/omap.html
-# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
-# This is commonly needed for tree structures. Example:
-#
-# --- !omap
-# - parent:
-# id: 1
-# parent_id: NULL
-# title: Parent
-# - child:
-# id: 2
-# parent_id: 1
-# title: Child
-#
-# = Using fixtures in testcases
-#
-# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
-# fixtures, but first let's take a look at a sample unit test:
-#
-# require 'test_helper'
-#
-# class WebSiteTest < ActiveSupport::TestCase
-# test "web_site_count" do
-# assert_equal 2, WebSite.count
-# end
-# end
-#
-# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database,
-# so this test will succeed.
-# The testing environment will automatically load the all fixtures into the database before each test.
-# To ensure consistent data, the environment deletes the fixtures before running the load.
-#
-# In addition to being available in the database, the fixture's data may also be accessed by
-# using a special dynamic method, which has the same name as the model, and accepts the
-# name of the fixture to instantiate:
-#
-# test "find" do
-# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-# end
-#
-# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
-#
-# test "find_alt_method_1" do
-# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
-# end
-#
-# test "find_alt_method_2" do
-# assert_equal "Ruby on Rails", @rubyonrails.news
-# end
-#
-# In order to use these methods to access fixtured data within your testcases, you must specify one of the
-# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
-#
-# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
-# self.use_instantiated_fixtures = true
-#
-# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
-# self.use_instantiated_fixtures = :no_instances
-#
-# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
-# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
-# large sets of fixtured data.
-#
-# = Dynamic fixtures with ERB
-#
-# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
-# mix ERB in with your YAML fixtures to create a bunch of fixtures for load testing, like:
-#
-# <% for i in 1..1000 %>
-# fix_<%= i %>:
-# id: <%= i %>
-# name: guy_<%= 1 %>
-# <% end %>
-#
-# This will create 1000 very simple YAML fixtures.
-#
-# Using ERB, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
-# This is however a feature to be used with some caution. The point of fixtures are that they're
-# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
-# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
-# in fixtures are to be considered a code smell.
-#
-# = Transactional fixtures
-#
-# TestCases can use begin+rollback to isolate their changes to the database instead of having to
-# delete+insert for every test case.
-#
-# class FooTest < ActiveSupport::TestCase
-# self.use_transactional_fixtures = true
-#
-# test "godzilla" do
-# assert !Foo.find(:all).empty?
-# Foo.destroy_all
-# assert Foo.find(:all).empty?
-# end
-#
-# test "godzilla aftermath" do
-# assert !Foo.find(:all).empty?
-# end
-# end
-#
-# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
-# 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.
-#
-# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
-# access to fixture data for every table that has been loaded through fixtures (depending on the
-# value of +use_instantiated_fixtures+)
-#
-# When *not* to use transactional fixtures:
-#
-# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
-# all parent transactions commit, particularly, the fixtures transaction which is begun in setup
-# and rolled back in teardown. Thus, you won't be able to verify
-# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-# Use InnoDB, MaxDB, or NDB instead.
-#
-# = Advanced YAML Fixtures
-#
-# YAML fixtures that don't specify an ID get some extra features:
-#
-# * Stable, autogenerated IDs
-# * Label references for associations (belongs_to, has_one, has_many)
-# * HABTM associations as inline lists
-# * Autofilled timestamp columns
-# * Fixture label interpolation
-# * Support for YAML defaults
-#
-# == Stable, autogenerated IDs
-#
-# Here, have a monkey fixture:
-#
-# george:
-# id: 1
-# name: George the Monkey
-#
-# reginald:
-# id: 2
-# name: Reginald the Pirate
-#
-# Each of these fixtures has two unique identifiers: one for the database
-# and one for the humans. Why don't we generate the primary key instead?
-# Hashing each fixture's label yields a consistent ID:
-#
-# george: # generated id: 503576764
-# name: George the Monkey
-#
-# reginald: # generated id: 324201669
-# name: Reginald the Pirate
-#
-# Active Record looks at the fixture's model class, discovers the correct
-# primary key, and generates it right before inserting the fixture
-# into the database.
-#
-# The generated ID for a given label is constant, so we can discover
-# any fixture's ID without loading anything, as long as we know the label.
-#
-# == Label references for associations (belongs_to, has_one, has_many)
-#
-# Specifying foreign keys in fixtures can be very fragile, not to
-# mention difficult to read. Since Active Record can figure out the ID of
-# any fixture from its label, you can specify FK's by label instead of ID.
-#
-# === belongs_to
-#
-# Let's break out some more monkeys and pirates.
-#
-# ### in pirates.yml
-#
-# reginald:
-# id: 1
-# name: Reginald the Pirate
-# monkey_id: 1
-#
-# ### in monkeys.yml
-#
-# george:
-# id: 1
-# name: George the Monkey
-# pirate_id: 1
-#
-# Add a few more monkeys and pirates and break this into multiple files,
-# and it gets pretty hard to keep track of what's going on. Let's
-# use labels instead of IDs:
-#
-# ### in pirates.yml
-#
-# reginald:
-# name: Reginald the Pirate
-# monkey: george
-#
-# ### in monkeys.yml
-#
-# george:
-# name: George the Monkey
-# pirate: reginald
-#
-# Pow! All is made clear. Active Record reflects on the fixture's model class,
-# finds all the +belongs_to+ associations, and allows you to specify
-# a target *label* for the *association* (monkey: george) rather than
-# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
-#
-# ==== Polymorphic belongs_to
-#
-# Supporting polymorphic relationships is a little bit more complicated, since
-# Active Record needs to know what type your association is pointing at. Something
-# like this should look familiar:
-#
-# ### in fruit.rb
-#
-# belongs_to :eater, :polymorphic => true
-#
-# ### in fruits.yml
-#
-# apple:
-# id: 1
-# name: apple
-# eater_id: 1
-# eater_type: Monkey
-#
-# Can we do better? You bet!
-#
-# apple:
-# eater: george (Monkey)
-#
-# Just provide the polymorphic target type and Active Record will take care of the rest.
-#
-# === has_and_belongs_to_many
-#
-# Time to give our monkey some fruit.
-#
-# ### in monkeys.yml
-#
-# george:
-# id: 1
-# name: George the Monkey
-#
-# ### in fruits.yml
-#
-# apple:
-# id: 1
-# name: apple
-#
-# orange:
-# id: 2
-# name: orange
-#
-# grape:
-# id: 3
-# name: grape
-#
-# ### in fruits_monkeys.yml
-#
-# apple_george:
-# fruit_id: 1
-# monkey_id: 1
-#
-# orange_george:
-# fruit_id: 2
-# monkey_id: 1
-#
-# grape_george:
-# fruit_id: 3
-# monkey_id: 1
-#
-# Let's make the HABTM fixture go away.
-#
-# ### in monkeys.yml
-#
-# george:
-# id: 1
-# name: George the Monkey
-# fruits: apple, orange, grape
-#
-# ### in fruits.yml
-#
-# apple:
-# name: apple
-#
-# orange:
-# name: orange
-#
-# grape:
-# name: grape
-#
-# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
-# on George's fixture, but we could've just as easily specified a list
-# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
-# the fixture's model class and discovers the +has_and_belongs_to_many+
-# associations.
-#
-# == Autofilled timestamp columns
-#
-# If your table/model specifies any of Active Record's
-# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
-# they will automatically be set to <tt>Time.now</tt>.
-#
-# If you've set specific values, they'll be left alone.
-#
-# == Fixture label interpolation
-#
-# The label of the current fixture is always available as a column value:
-#
-# geeksomnia:
-# name: Geeksomnia's Account
-# subdomain: $LABEL
-#
-# Also, sometimes (like when porting older join table fixtures) you'll need
-# to be able to get a hold of the identifier for a given label. ERB
-# to the rescue:
-#
-# george_reginald:
-# monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %>
-# pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %>
-#
-# == Support for YAML defaults
-#
-# You probably already know how to use YAML to set and reuse defaults in
-# your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
-#
-# DEFAULTS: &DEFAULTS
-# created_on: <%= 3.weeks.ago.to_s(:db) %>
-#
-# first:
-# name: Smurf
-# <<: *DEFAULTS
-#
-# second:
-# name: Fraggle
-# <<: *DEFAULTS
-#
-# Any fixture labeled "DEFAULTS" is safely ignored.
-
-Fixture = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixture', 'ActiveRecord::Fixture')
-Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixtures', 'ActiveRecord::Fixtures')
-
module ActiveRecord
+ # \Fixtures are a way of organizing data that you want to test against; in short, sample data.
+ #
+ # They are stored in YAML files, one file per model, which are placed in the directory
+ # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
+ # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
+ # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
+ # like this:
+ #
+ # rubyonrails:
+ # id: 1
+ # name: Ruby on Rails
+ # url: http://www.rubyonrails.org
+ #
+ # google:
+ # id: 2
+ # name: Google
+ # url: http://www.google.com
+ #
+ # This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and
+ # is followed by an indented list of key/value pairs in the "key: value" format. Records are
+ # separated by a blank line for your viewing pleasure.
+ #
+ # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+ # See http://yaml.org/type/omap.html
+ # for the specification. You will need ordered fixtures when you have foreign key constraints
+ # on keys in the same table. This is commonly needed for tree structures. Example:
+ #
+ # --- !omap
+ # - parent:
+ # id: 1
+ # parent_id: NULL
+ # title: Parent
+ # - child:
+ # id: 2
+ # parent_id: 1
+ # title: Child
+ #
+ # = Using Fixtures in Test Cases
+ #
+ # Since fixtures are a testing construct, we use them in our unit and functional tests. There
+ # are two ways to use the fixtures, but first let's take a look at a sample unit test:
+ #
+ # require 'test_helper'
+ #
+ # class WebSiteTest < ActiveSupport::TestCase
+ # test "web_site_count" do
+ # assert_equal 2, WebSite.count
+ # end
+ # end
+ #
+ # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
+ # so this test will succeed.
+ #
+ # The testing environment will automatically load the all fixtures into the database before each
+ # test. To ensure consistent data, the environment deletes the fixtures before running the load.
+ #
+ # In addition to being available in the database, the fixture's data may also be accessed by
+ # using a special dynamic method, which has the same name as the model, and accepts the
+ # name of the fixture to instantiate:
+ #
+ # test "find" do
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
+ # end
+ #
+ # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
+ # following tests:
+ #
+ # test "find_alt_method_1" do
+ # assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
+ # end
+ #
+ # test "find_alt_method_2" do
+ # assert_equal "Ruby on Rails", @rubyonrails.news
+ # end
+ #
+ # In order to use these methods to access fixtured data within your testcases, you must specify one of the
+ # following in your <tt>ActiveSupport::TestCase</tt>-derived class:
+ #
+ # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
+ # self.use_instantiated_fixtures = true
+ #
+ # - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
+ # self.use_instantiated_fixtures = :no_instances
+ #
+ # Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
+ # traversed in the database to create the fixture hash and/or instance variables. This is expensive for
+ # large sets of fixtured data.
+ #
+ # = Dynamic fixtures with ERB
+ #
+ # Some times you don't care about the content of the fixtures as much as you care about the volume.
+ # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
+ # testing, like:
+ #
+ # <% 1.upto(1000) do |i| %>
+ # fix_<%= i %>:
+ # id: <%= i %>
+ # name: guy_<%= 1 %>
+ # <% end %>
+ #
+ # This will create 1000 very simple fixtures.
+ #
+ # Using ERB, you can also inject dynamic values into your fixtures with inserts like
+ # <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
+ # This is however a feature to be used with some caution. The point of fixtures are that they're
+ # stable units of predictable sample data. If you feel that you need to inject dynamic values, then
+ # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
+ # in fixtures are to be considered a code smell.
+ #
+ # = Transactional Fixtures
+ #
+ # Test cases can use begin+rollback to isolate their changes to the database instead of having to
+ # delete+insert for every test case.
+ #
+ # class FooTest < ActiveSupport::TestCase
+ # self.use_transactional_fixtures = true
+ #
+ # test "godzilla" do
+ # assert !Foo.all.empty?
+ # Foo.destroy_all
+ # assert Foo.all.empty?
+ # end
+ #
+ # test "godzilla aftermath" do
+ # assert !Foo.all.empty?
+ # end
+ # end
+ #
+ # If you preload your test database with all fixture data (probably in the rake task) and use
+ # transactional fixtures, 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.
+ #
+ # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
+ # true. This will provide access to fixture data for every table that has been loaded through
+ # fixtures (depending on the value of +use_instantiated_fixtures+).
+ #
+ # When *not* to use transactional fixtures:
+ #
+ # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
+ # all parent transactions commit, particularly, the fixtures transaction which is begun in setup
+ # and rolled back in teardown. Thus, you won't be able to verify
+ # the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
+ # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
+ # Use InnoDB, MaxDB, or NDB instead.
+ #
+ # = Advanced Fixtures
+ #
+ # Fixtures that don't specify an ID get some extra features:
+ #
+ # * Stable, autogenerated IDs
+ # * Label references for associations (belongs_to, has_one, has_many)
+ # * HABTM associations as inline lists
+ # * Autofilled timestamp columns
+ # * Fixture label interpolation
+ # * Support for YAML defaults
+ #
+ # == Stable, Autogenerated IDs
+ #
+ # Here, have a monkey fixture:
+ #
+ # george:
+ # id: 1
+ # name: George the Monkey
+ #
+ # reginald:
+ # id: 2
+ # name: Reginald the Pirate
+ #
+ # Each of these fixtures has two unique identifiers: one for the database
+ # and one for the humans. Why don't we generate the primary key instead?
+ # Hashing each fixture's label yields a consistent ID:
+ #
+ # george: # generated id: 503576764
+ # name: George the Monkey
+ #
+ # reginald: # generated id: 324201669
+ # name: Reginald the Pirate
+ #
+ # Active Record looks at the fixture's model class, discovers the correct
+ # primary key, and generates it right before inserting the fixture
+ # into the database.
+ #
+ # The generated ID for a given label is constant, so we can discover
+ # any fixture's ID without loading anything, as long as we know the label.
+ #
+ # == Label references for associations (belongs_to, has_one, has_many)
+ #
+ # Specifying foreign keys in fixtures can be very fragile, not to
+ # mention difficult to read. Since Active Record can figure out the ID of
+ # any fixture from its label, you can specify FK's by label instead of ID.
+ #
+ # === belongs_to
+ #
+ # Let's break out some more monkeys and pirates.
+ #
+ # ### in pirates.yml
+ #
+ # reginald:
+ # id: 1
+ # name: Reginald the Pirate
+ # monkey_id: 1
+ #
+ # ### in monkeys.yml
+ #
+ # george:
+ # id: 1
+ # name: George the Monkey
+ # pirate_id: 1
+ #
+ # Add a few more monkeys and pirates and break this into multiple files,
+ # and it gets pretty hard to keep track of what's going on. Let's
+ # use labels instead of IDs:
+ #
+ # ### in pirates.yml
+ #
+ # reginald:
+ # name: Reginald the Pirate
+ # monkey: george
+ #
+ # ### in monkeys.yml
+ #
+ # george:
+ # name: George the Monkey
+ # pirate: reginald
+ #
+ # Pow! All is made clear. Active Record reflects on the fixture's model class,
+ # finds all the +belongs_to+ associations, and allows you to specify
+ # a target *label* for the *association* (monkey: george) rather than
+ # a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
+ #
+ # ==== Polymorphic belongs_to
+ #
+ # Supporting polymorphic relationships is a little bit more complicated, since
+ # Active Record needs to know what type your association is pointing at. Something
+ # like this should look familiar:
+ #
+ # ### in fruit.rb
+ #
+ # belongs_to :eater, :polymorphic => true
+ #
+ # ### in fruits.yml
+ #
+ # apple:
+ # id: 1
+ # name: apple
+ # eater_id: 1
+ # eater_type: Monkey
+ #
+ # Can we do better? You bet!
+ #
+ # apple:
+ # eater: george (Monkey)
+ #
+ # Just provide the polymorphic target type and Active Record will take care of the rest.
+ #
+ # === has_and_belongs_to_many
+ #
+ # Time to give our monkey some fruit.
+ #
+ # ### in monkeys.yml
+ #
+ # george:
+ # id: 1
+ # name: George the Monkey
+ #
+ # ### in fruits.yml
+ #
+ # apple:
+ # id: 1
+ # name: apple
+ #
+ # orange:
+ # id: 2
+ # name: orange
+ #
+ # grape:
+ # id: 3
+ # name: grape
+ #
+ # ### in fruits_monkeys.yml
+ #
+ # apple_george:
+ # fruit_id: 1
+ # monkey_id: 1
+ #
+ # orange_george:
+ # fruit_id: 2
+ # monkey_id: 1
+ #
+ # grape_george:
+ # fruit_id: 3
+ # monkey_id: 1
+ #
+ # Let's make the HABTM fixture go away.
+ #
+ # ### in monkeys.yml
+ #
+ # george:
+ # id: 1
+ # name: George the Monkey
+ # fruits: apple, orange, grape
+ #
+ # ### in fruits.yml
+ #
+ # apple:
+ # name: apple
+ #
+ # orange:
+ # name: orange
+ #
+ # grape:
+ # name: grape
+ #
+ # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
+ # on George's fixture, but we could've just as easily specified a list
+ # of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
+ # the fixture's model class and discovers the +has_and_belongs_to_many+
+ # associations.
+ #
+ # == Autofilled Timestamp Columns
+ #
+ # If your table/model specifies any of Active Record's
+ # standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
+ # they will automatically be set to <tt>Time.now</tt>.
+ #
+ # If you've set specific values, they'll be left alone.
+ #
+ # == Fixture label interpolation
+ #
+ # The label of the current fixture is always available as a column value:
+ #
+ # geeksomnia:
+ # name: Geeksomnia's Account
+ # subdomain: $LABEL
+ #
+ # Also, sometimes (like when porting older join table fixtures) you'll need
+ # to be able to get a hold of the identifier for a given label. ERB
+ # to the rescue:
+ #
+ # george_reginald:
+ # monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %>
+ # pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %>
+ #
+ # == Support for YAML defaults
+ #
+ # You probably already know how to use YAML to set and reuse defaults in
+ # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
+ #
+ # DEFAULTS: &DEFAULTS
+ # created_on: <%= 3.weeks.ago.to_s(:db) %>
+ #
+ # first:
+ # name: Smurf
+ # *DEFAULTS
+ #
+ # second:
+ # name: Fraggle
+ # *DEFAULTS
+ #
+ # Any fixture labeled "DEFAULTS" is safely ignored.
class Fixtures
MAX_ID = 2 ** 30 - 1
@@ -477,7 +466,7 @@ module ActiveRecord
connection,
table_name,
class_names[table_name.to_sym] || table_name.classify,
- File.join(fixtures_directory, path))
+ ::File.join(fixtures_directory, path))
end
all_loaded_fixtures.update(fixtures_map)
@@ -558,7 +547,7 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ # Return a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
@@ -655,74 +644,33 @@ module ActiveRecord
end
def read_fixture_files
- if File.file?(yaml_file_path)
+ if ::File.file?(yaml_file_path)
read_yaml_fixture_files
- elsif File.file?(csv_file_path)
- read_csv_fixture_files
else
- raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
+ raise FixturesFileNotFound, "Could not find #{yaml_file_path}"
end
end
def read_yaml_fixture_files
- yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f|
- File.file?(f)
- } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join
-
- if yaml = parse_yaml_string(yaml_string)
- # If the file is an ordered map, extract its children.
- yaml_value =
- if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
- yaml.value
- else
- [yaml]
- end
-
- yaml_value.each do |fixture|
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
- fixture.each do |name, data|
- unless data
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
- end
-
- fixtures[name] = ActiveRecord::Fixture.new(data, model_class)
+ yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
+ ::File.file?(f)
+ } + [yaml_file_path]
+
+ yaml_files.each do |file|
+ Fixtures::File.open(file) do |fh|
+ fh.each do |name, row|
+ fixtures[name] = ActiveRecord::Fixture.new(row, model_class)
end
end
end
end
- def read_csv_fixture_files
- reader = CSV.parse(erb_render(IO.read(csv_file_path)))
- header = reader.shift
- i = 0
- reader.each do |row|
- data = {}
- row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
- fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = ActiveRecord::Fixture.new(data, model_class)
- end
- end
- deprecate :read_csv_fixture_files
-
def yaml_file_path
"#{@fixture_path}.yml"
end
- def csv_file_path
- @fixture_path + ".csv"
- end
-
def yaml_fixtures_key(path)
- File.basename(@fixture_path).split(".").first
- end
-
- def parse_yaml_string(fixture_content)
- YAML::load(erb_render(fixture_content))
- rescue => error
- raise Fixture::FormatError, "a YAML error occurred 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\nThe exact error was:\n #{error.class}: #{error}"
- end
-
- def erb_render(fixture_content)
- ERB.new(fixture_content).result
+ ::File.basename(@fixture_path).split(".").first
end
end
@@ -798,7 +746,7 @@ module ActiveRecord
def fixtures(*fixture_names)
if fixture_names.first == :all
- fixture_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
+ fixture_names = Dir["#{fixture_path}/**/*.{yml}"]
fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
else
fixture_names = fixture_names.flatten.map { |n| n.to_s }
@@ -894,9 +842,12 @@ module ActiveRecord
@loaded_fixtures = load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
- ActiveRecord::Base.connection.increment_open_transactions
- ActiveRecord::Base.connection.transaction_joinable = false
- ActiveRecord::Base.connection.begin_db_transaction
+ @fixture_connections = enlist_fixture_connections
+ @fixture_connections.each do |connection|
+ connection.increment_open_transactions
+ connection.transaction_joinable = false
+ connection.begin_db_transaction
+ end
# Load fixtures for every test.
else
ActiveRecord::Fixtures.reset_cache
@@ -916,13 +867,22 @@ module ActiveRecord
end
# Rollback changes if a transaction is active.
- if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
- ActiveRecord::Base.connection.rollback_db_transaction
- ActiveRecord::Base.connection.decrement_open_transactions
+ if run_in_transaction?
+ @fixture_connections.each do |connection|
+ if connection.open_transactions != 0
+ connection.rollback_db_transaction
+ connection.decrement_open_transactions
+ end
+ end
+ @fixture_connections.clear
end
ActiveRecord::Base.clear_active_connections!
end
+ def enlist_fixture_connections
+ ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
+ end
+
private
def load_fixtures
fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb
new file mode 100644
index 0000000000..6bad36abb9
--- /dev/null
+++ b/activerecord/lib/active_record/fixtures/file.rb
@@ -0,0 +1,65 @@
+begin
+ require 'psych'
+rescue LoadError
+end
+
+require 'erb'
+require 'yaml'
+
+module ActiveRecord
+ class Fixtures
+ class File
+ include Enumerable
+
+ ##
+ # Open a fixture file named +file+. When called with a block, the block
+ # is called with the filehandle and the filehandle is automatically closed
+ # when the block finishes.
+ def self.open(file)
+ x = new file
+ block_given? ? yield(x) : x
+ end
+
+ def initialize(file)
+ @file = file
+ @rows = nil
+ end
+
+ def each(&block)
+ rows.each(&block)
+ end
+
+ RESCUE_ERRORS = [ ArgumentError ] # :nodoc:
+
+ private
+ if defined?(Psych) && defined?(Psych::SyntaxError)
+ RESCUE_ERRORS << Psych::SyntaxError
+ end
+
+ def rows
+ return @rows if @rows
+
+ begin
+ data = YAML.load(render(IO.read(@file)))
+ rescue *RESCUE_ERRORS => error
+ raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. 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\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
+ end
+ @rows = data ? validate(data).to_a : []
+ end
+
+ def render(content)
+ ERB.new(content).result
+ end
+
+ # Validate our unmarshalled data.
+ def validate(data)
+ unless Hash === data || YAML::Omap === data
+ raise Fixture::FormatError, 'fixture is not a hash'
+ end
+
+ raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
+ data
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index cdedcde0eb..d9ad7e4132 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -3,16 +3,17 @@ module ActiveRecord
# == What is Optimistic Locking
#
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
- # conflicts with the data. It does this by checking whether another process has made changes to a record since
- # it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
+ # conflicts with the data. It does this by checking whether another process has made changes to a record since
+ # it was opened, an <tt>ActiveRecord::StaleObjectError</tt> exception is thrown if that has occurred
+ # and the update is ignored.
#
- # Check out ActiveRecord::Locking::Pessimistic for an alternative.
+ # Check out <tt>ActiveRecord::Locking::Pessimistic</tt> for an alternative.
#
# == Usage
#
- # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
- # record increments the lock_version column and the locking facilities ensure that records instantiated twice
- # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
+ # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
+ # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
+ # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
@@ -36,10 +37,10 @@ module ActiveRecord
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
- # You must ensure that your database schema defaults the lock_version column to 0.
+ # You must ensure that your database schema defaults the +lock_version+ column to 0.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
- # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
+ # To override the name of the +lock_version+ column, invoke the <tt>set_locking_column</tt> method.
# This method uses the same syntax as <tt>set_table_name</tt>
module Optimistic
extend ActiveSupport::Concern
@@ -68,11 +69,11 @@ module ActiveRecord
result = super
# If the locking column has no default value set,
- # start the lock version at zero. Note we can't use
- # locking_enabled? at this point as @attributes may
- # not have been initialized yet
+ # start the lock version at zero. Note we can't use
+ # <tt>locking_enabled?</tt> at this point as
+ # <tt>@attributes</tt> may not have been initialized yet.
- if lock_optimistically && result.include?(self.class.locking_column)
+ if result.key?(self.class.locking_column) && lock_optimistically
result[self.class.locking_column] ||= 0
end
@@ -99,7 +100,7 @@ module ActiveRecord
)
).arel.compile_update(arel_attributes_values(false, false, attribute_names))
- affected_rows = connection.update stmt.to_sql
+ affected_rows = connection.update stmt
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
@@ -137,10 +138,9 @@ module ActiveRecord
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
- # Is optimistic locking enabled for this table? Returns true if the
- # +lock_optimistically+ flag is set to true (which it is, by default)
- # and the table includes the +locking_column+ column (defaults to
- # +lock_version+).
+ # Returns true if the +lock_optimistically+ flag is set to true
+ # (which it is, by default) and the table includes the
+ # +locking_column+ column (defaults to +lock_version+).
def locking_enabled?
lock_optimistically && columns_hash[locking_column]
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 862cf8f72a..66994e4797 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,7 +3,7 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>:lock => true</tt> to ActiveRecord::Base.find to obtain an exclusive
+ # Pass <tt>:lock => true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
# Account.find(1, :lock => true)
@@ -14,14 +14,14 @@ module ActiveRecord
# Account.transaction do
# # select * from accounts where name = 'shugo' limit 1 for update
# shugo = Account.where("name = 'shugo'").lock(true).first
- # yuko = Account.where("name = 'shugo'").lock(true).first
+ # yuko = Account.where("name = 'yuko'").lock(true).first
# shugo.balance -= 100
# shugo.save!
# yuko.balance += 100
# yuko.save!
# end
#
- # You can also use ActiveRecord::Base#lock! method to lock one record by id.
+ # You can also use <tt>ActiveRecord::Base#lock!</tt> method to lock one record by id.
# This may be better if you don't need to lock every row. Example:
#
# Account.transaction do
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 640111096d..7166f1b82a 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,3 +1,5 @@
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/class/attribute_accessors"
require "active_support/core_ext/array/wrap"
module ActiveRecord
@@ -53,7 +55,7 @@ module ActiveRecord
#
# This migration will add a boolean flag to the accounts table and remove it
# if you're backing out of the migration. It shows how all migrations have
- # two class methods +up+ and +down+ that describes the transformations
+ # two methods +up+ and +down+ that describes the transformations
# required to implement or remove the migration. These methods can consist
# of both the migration specific methods like add_column and remove_column,
# but may also contain regular Ruby code for generating data needed for the
@@ -66,9 +68,9 @@ module ActiveRecord
# create_table :system_settings do |t|
# t.string :name
# t.string :label
- # t.text :value
+ # t.text :value
# t.string :type
- # t.integer :position
+ # t.integer :position
# end
#
# SystemSetting.create :name => "notice",
@@ -116,8 +118,10 @@ module ActiveRecord
# with the name of the column. Other options include
# <tt>:name</tt> and <tt>:unique</tt> (e.g.
# <tt>{ :name => "users_name_index", :unique => true }</tt>).
- # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified
- # by +index_name+.
+ # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index
+ # specified by +column_name+.
+ # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index
+ # specified by +index_name+.
#
# == Irreversible transformations
#
@@ -179,7 +183,7 @@ module ActiveRecord
#
# class RemoveEmptyTags < ActiveRecord::Migration
# def up
- # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
+ # Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
# end
#
# def down
@@ -225,7 +229,7 @@ module ActiveRecord
# def up
# add_column :people, :salary, :integer
# Person.reset_column_information
- # Person.find(:all).each do |p|
+ # Person.all.each do |p|
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
@@ -245,7 +249,7 @@ module ActiveRecord
# def up
# ...
# say_with_time "Updating salaries..." do
- # Person.find(:all).each do |p|
+ # Person.all.each do |p|
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
@@ -328,6 +332,10 @@ module ActiveRecord
(delegate || superclass.delegate).send(name, *args, &block)
end
+ def self.migrate(direction)
+ new.migrate direction
+ end
+
cattr_accessor :verbose
attr_accessor :name, :version
@@ -555,7 +563,7 @@ module ActiveRecord
def get_all_versions
table = Arel::Table.new(schema_migrations_table_name)
- Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort
+ Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort
end
def current_version
@@ -712,11 +720,11 @@ module ActiveRecord
if down?
@migrated_versions.delete(version)
stmt = table.where(table["version"].eq(version.to_s)).compile_delete
- Base.connection.delete stmt.to_sql
+ Base.connection.delete stmt
else
@migrated_versions.push(version).sort!
stmt = table.compile_insert table["version"] => version.to_s
- Base.connection.insert stmt.to_sql
+ Base.connection.insert stmt
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index c9d57ce812..ffee5a081a 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -1,12 +1,12 @@
module ActiveRecord
class Migration
- # ActiveRecord::Migration::CommandRecorder records commands done during
- # a migration and knows how to reverse those commands. The CommandRecorder
+ # <tt>ActiveRecord::Migration::CommandRecorder</tt> records commands done during
+ # a migration and knows how to reverse those commands. The CommandRecorder
# knows how to invert the following commands:
#
# * add_column
# * add_index
- # * add_timestamp
+ # * add_timestamps
# * create_table
# * remove_timestamps
# * rename_column
@@ -20,21 +20,21 @@ module ActiveRecord
@delegate = delegate
end
- # record +command+. +command+ should be a method name and arguments.
+ # record +command+. +command+ should be a method name and arguments.
# For example:
#
- # recorder.record(:method_name, [:arg1, arg2])
+ # recorder.record(:method_name, [:arg1, :arg2])
def record(*command)
@commands << command
end
# Returns a list that represents commands that are the inverse of the
- # commands stored in +commands+. For example:
+ # commands stored in +commands+. For example:
#
# recorder.record(:rename_table, [:old, :new])
# recorder.inverse # => [:rename_table, [:new, :old]]
#
- # This method will raise an IrreversibleMigration exception if it cannot
+ # This method will raise an +IrreversibleMigration+ exception if it cannot
# invert the +commands+.
def inverse
@commands.reverse.map { |name, args|
@@ -48,11 +48,11 @@ module ActiveRecord
super || delegate.respond_to?(*args)
end
- [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
+ [:create_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{method}(*args)
- record(:"#{method}", args)
- end
+ def #{method}(*args) # def create_table(*args)
+ record(:"#{method}", args) # record(:create_table, args)
+ end # end
EOV
end
@@ -71,7 +71,7 @@ module ActiveRecord
end
def invert_rename_index(args)
- [:rename_index, args.reverse]
+ [:rename_index, [args.first] + args.last(2).reverse]
end
def invert_rename_column(args)
@@ -79,8 +79,10 @@ module ActiveRecord
end
def invert_add_index(args)
- table, columns, _ = *args
- [:remove_index, [table, {:column => columns}]]
+ table, columns, options = *args
+ index_name = options.try(:[], :name)
+ options_hash = index_name ? {:name => index_name} : {:column => columns}
+ [:remove_index, [table, options_hash]]
end
def invert_remove_timestamps(args)
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 588f52be44..0313abe456 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -17,7 +17,7 @@ module ActiveRecord
# posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
#
# fruits = Fruit.scoped
- # fruits = fruits.where(:colour => 'red') if options[:red_only]
+ # fruits = fruits.where(:color => 'red') if options[:red_only]
# fruits = fruits.limit(10) if limited?
#
# Anonymous \scopes tend to be useful when procedurally generating complex
@@ -40,6 +40,25 @@ module ActiveRecord
end
end
+ ##
+ # Collects attributes from scopes that should be applied when creating
+ # an AR instance for the particular class this is called on.
+ def scope_attributes # :nodoc:
+ if current_scope
+ current_scope.scope_for_create
+ else
+ scope = relation.clone
+ scope.default_scoped = true
+ scope.scope_for_create
+ end
+ end
+
+ ##
+ # Are there default attributes associated with this scope?
+ def scope_attributes? # :nodoc:
+ current_scope || default_scopes.any?
+ end
+
# Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
# such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
#
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 08b27b6a8e..2dbebfcaf8 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -277,14 +277,14 @@ module ActiveRecord
type = (reflection.collection? ? :collection : :one_to_one)
# def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
# end
class_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options)
end
eoruby
else
@@ -319,21 +319,21 @@ module ActiveRecord
# If the given attributes include a matching <tt>:id</tt> attribute, or
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
# then the existing record will be marked for destruction.
- def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
- elsif attributes['id'].present?
+ elsif attributes['id'].present? && !assignment_opts[:without_protection]
raise_nested_attributes_record_not_found(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
@@ -367,7 +367,7 @@ module ActiveRecord
# { :name => 'John' },
# { :id => '2', :_destroy => true }
# ])
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {})
options = self.nested_attributes_options[association_name]
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
@@ -383,7 +383,7 @@ module ActiveRecord
attributes_collection = if keys.include?('id') || keys.include?(:id)
Array.wrap(attributes_collection)
else
- attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
+ attributes_collection.values
end
end
@@ -401,7 +401,7 @@ module ActiveRecord
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
- association.build(attributes.except(*UNASSIGNABLE_KEYS))
+ association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
unless association.loaded? || call_reject_if(association_name, attributes)
@@ -418,8 +418,10 @@ module ActiveRecord
end
if !call_reject_if(association_name, attributes)
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts)
end
+ elsif assignment_opts[:without_protection]
+ association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise_nested_attributes_record_not_found(association_name, attributes['id'])
end
@@ -428,8 +430,8 @@ module ActiveRecord
# Updates a record with the +attributes+ or marks it for destruction if
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
- def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
- record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts)
+ record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
end
@@ -458,5 +460,9 @@ module ActiveRecord
def raise_nested_attributes_record_not_found(association_name, record_id)
raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
end
+
+ def unassignable_keys(assignment_opts)
+ assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS
+ end
end
end
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
index c723436330..fdf17c003c 100644
--- a/activerecord/lib/active_record/observer.rb
+++ b/activerecord/lib/active_record/observer.rb
@@ -11,7 +11,7 @@ module ActiveRecord
#
# class CommentObserver < ActiveRecord::Observer
# def after_save(comment)
- # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
+ # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
# end
# end
#
@@ -111,7 +111,7 @@ module ActiveRecord
callback_meth = :"_notify_#{observer_name}_for_#{callback}"
unless klass.respond_to?(callback_meth)
klass.send(:define_method, callback_meth) do |&block|
- observer.send(callback, self, &block)
+ observer.update(callback, self, &block)
end
klass.send(callback, callback_meth)
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b9041f44d8..5e65e46a7d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -33,7 +33,11 @@ module ActiveRecord
# +save+ returns +false+. See ActiveRecord::Callbacks for further
# details.
def save(*)
- create_or_update
+ begin
+ create_or_update
+ rescue ActiveRecord::RecordInvalid
+ false
+ end
end
# Saves the model.
@@ -75,6 +79,8 @@ module ActiveRecord
# Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
def destroy
+ destroy_associations
+
if persisted?
IdentityMap.remove(self) if IdentityMap.enabled?
pk = self.class.primary_key
@@ -133,6 +139,8 @@ module ActiveRecord
# * Callbacks are skipped.
# * updated_at/updated_on column is not updated if that column is available.
#
+ # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
+ # attribute is marked as readonly.
def update_column(name, value)
name = name.to_s
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
@@ -273,11 +281,16 @@ module ActiveRecord
@changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
- self.class.update_all(changes, { primary_key => self[primary_key] }) == 1
+ self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1
end
end
private
+
+ # A hook to be overridden by association modules.
+ def destroy_associations
+ end
+
def create_or_update
raise ReadOnlyRecord if readonly?
result = new_record? ? create : update
@@ -291,7 +304,7 @@ module ActiveRecord
return 0 if attributes_with_values.empty?
klass = self.class
stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
- klass.connection.update stmt.to_sql
+ klass.connection.update stmt
end
# Creates a record with values matching those of the instance attributes
@@ -301,7 +314,7 @@ module ActiveRecord
new_id = self.class.unscoped.insert attributes_values
- self.id ||= new_id
+ self.id ||= new_id if self.class.primary_key
IdentityMap.add(self) if IdentityMap.enabled?
@new_record = false
@@ -313,9 +326,7 @@ module ActiveRecord
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
# that instances loaded from the database would.
def attributes_from_column_definition
- Hash[self.class.columns.map do |column|
- [column.name, column.default]
- end]
+ self.class.column_defaults.dup
end
end
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 4e61671473..466d148901 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -28,9 +28,18 @@ module ActiveRecord
end
class BodyProxy # :nodoc:
- def initialize(original_cache_value, target)
+ def initialize(original_cache_value, target, connection_id)
@original_cache_value = original_cache_value
@target = target
+ @connection_id = connection_id
+ end
+
+ def method_missing(method_sym, *arguments, &block)
+ @target.send(method_sym, *arguments, &block)
+ end
+
+ def respond_to?(method_sym, include_private = false)
+ super || @target.respond_to?(method_sym)
end
def each(&block)
@@ -40,6 +49,7 @@ module ActiveRecord
def close
@target.close if @target.respond_to?(:close)
ensure
+ ActiveRecord::Base.connection_id = @connection_id
ActiveRecord::Base.connection.clear_query_cache
unless @original_cache_value
ActiveRecord::Base.connection.disable_query_cache!
@@ -52,7 +62,13 @@ module ActiveRecord
ActiveRecord::Base.connection.enable_query_cache!
status, headers, body = @app.call(env)
- [status, headers, BodyProxy.new(old, body)]
+ [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)]
+ rescue Exception => e
+ ActiveRecord::Base.connection.clear_query_cache
+ unless old
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+ raise e
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index bae2ded244..47133e77e8 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -29,8 +29,8 @@ module ActiveRecord
# When loading console, force ActiveRecord::Base to be loaded
# to avoid cross references when loading a constant for the
# first time. Also, make it output to STDERR.
- console do |sandbox|
- require "active_record/railties/console_sandbox" if sandbox
+ console do |app|
+ require "active_record/railties/console_sandbox" if app.sandbox?
ActiveRecord::Base.logger = Logger.new(STDERR)
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index bc6ca936c0..c5db9b4625 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -1,14 +1,23 @@
require 'active_support/core_ext/module/attr_internal'
+require 'active_record/log_subscriber'
module ActiveRecord
module Railties
- module ControllerRuntime
+ module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
protected
attr_internal :db_runtime
+ def process_action(action, *args)
+ # We also need to reset the runtime before each action
+ # because of queries in middleware or in cases we are streaming
+ # and it won't be cleaned up by the method below.
+ ActiveRecord::LogSubscriber.reset_runtime
+ super
+ end
+
def cleanup_view_runtime
if ActiveRecord::Base.connected?
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
@@ -23,7 +32,9 @@ module ActiveRecord
def append_info_to_payload(payload)
super
- payload[:db_runtime] = db_runtime
+ if ActiveRecord::Base.connected?
+ payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ end
end
module ClassMethods
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index bcdabb0f31..4fb19b14ea 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -27,7 +27,7 @@ db_namespace = namespace :db do
#
# development:
# database: blog_development
- # <<: *defaults
+ # *defaults
next unless config['database']
# Only connect to local databases
local_database?(config) { create_database(config) }
@@ -44,6 +44,12 @@ db_namespace = namespace :db do
create_database(ActiveRecord::Base.configurations[Rails.env])
end
+ def mysql_creation_options(config)
+ @charset = ENV['CHARSET'] || 'utf8'
+ @collation = ENV['COLLATION'] || 'utf8_unicode_ci'
+ {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
+ end
+
def create_database(config)
begin
if config['adapter'] =~ /sqlite/
@@ -67,9 +73,6 @@ db_namespace = namespace :db do
rescue
case config['adapter']
when /mysql/
- @charset = ENV['CHARSET'] || 'utf8'
- @collation = ENV['COLLATION'] || 'utf8_unicode_ci'
- creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
if config['adapter'] =~ /jdbc/
#FIXME After Jdbcmysql gives this class
require 'active_record/railties/jdbcmysql_error'
@@ -80,7 +83,7 @@ db_namespace = namespace :db do
access_denied_error = 1045
begin
ActiveRecord::Base.establish_connection(config.merge('database' => nil))
- ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config))
ActiveRecord::Base.establish_connection(config)
rescue error_class => sqlerr
if sqlerr.errno == access_denied_error
@@ -91,7 +94,7 @@ db_namespace = namespace :db do
"IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;"
ActiveRecord::Base.establish_connection(config.merge(
'database' => nil, 'username' => 'root', 'password' => root_password))
- ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config))
ActiveRecord::Base.connection.execute grant_statement
ActiveRecord::Base.establish_connection(config)
else
@@ -112,7 +115,8 @@ db_namespace = namespace :db do
end
end
else
- $stderr.puts "#{config['database']} already exists"
+ # Bug with 1.9.2 Calling return within begin still executes else
+ $stderr.puts "#{config['database']} already exists" unless config['adapter'] =~ /sqlite/
end
end
@@ -199,11 +203,13 @@ db_namespace = namespace :db do
end
db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
file_list = []
- Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file|
- # only files matching "20091231235959_some_name.rb" pattern
- if match_data = /^(\d{14})_(.+)\.rb$/.match(file)
- status = db_list.delete(match_data[1]) ? 'up' : 'down'
- file_list << [status, match_data[1], match_data[2].humanize]
+ ActiveRecord::Migrator.migrations_paths.each do |path|
+ Dir.foreach(path) do |file|
+ # only files matching "20091231235959_some_name.rb" pattern
+ if match_data = /^(\d{14})_(.+)\.rb$/.match(file)
+ status = db_list.delete(match_data[1]) ? 'up' : 'down'
+ file_list << [status, match_data[1], match_data[2].humanize]
+ end
end
end
db_list.map! do |version|
@@ -296,7 +302,7 @@ db_namespace = namespace :db do
end
namespace :fixtures do
- desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task :load => :environment do
require 'active_record/fixtures'
@@ -335,9 +341,11 @@ db_namespace = namespace :db do
namespace :schema do
desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
- task :dump => :load_config do
+ task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
- File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file|
+ filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::Base.establish_connection(Rails.env)
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
db_namespace['schema:dump'].reenable
@@ -368,7 +376,7 @@ db_namespace = namespace :db do
ENV['PGPASSWORD'] = abcs[Rails.env]['password'].to_s if abcs[Rails.env]['password']
search_path = abcs[Rails.env]['schema_search_path']
unless search_path.blank?
- search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ")
+ search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ")
end
`pg_dump -i -U "#{abcs[Rails.env]['username']}" -s -x -O -f db/#{Rails.env}_structure.sql #{search_path} #{abcs[Rails.env]['database']}`
raise 'Error dumping database' if $?.exitstatus == 1
@@ -376,8 +384,7 @@ db_namespace = namespace :db do
dbfile = abcs[Rails.env]['database'] || abcs[Rails.env]['dbfile']
`sqlite3 #{dbfile} .schema > db/#{Rails.env}_structure.sql`
when 'sqlserver'
- `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /f db\\#{Rails.env}_structure.sql /q /A /r`
- `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /F db\ /q /A /r`
+ `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\#{Rails.env}_structure.sql -A -U`
when "firebird"
set_firebird_env(abcs[Rails.env])
db_string = firebird_db_string(abcs[Rails.env])
@@ -417,12 +424,12 @@ db_namespace = namespace :db do
ENV['PGHOST'] = abcs['test']['host'] if abcs['test']['host']
ENV['PGPORT'] = abcs['test']['port'].to_s if abcs['test']['port']
ENV['PGPASSWORD'] = abcs['test']['password'].to_s if abcs['test']['password']
- `psql -U "#{abcs['test']['username']}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs['test']['database']} #{abcs['test']['template']}`
+ `psql -U "#{abcs['test']['username']}" -f "#{Rails.root}/db/#{Rails.env}_structure.sql" #{abcs['test']['database']} #{abcs['test']['template']}`
when /sqlite/
dbfile = abcs['test']['database'] || abcs['test']['dbfile']
- `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql`
+ `sqlite3 #{dbfile} < "#{Rails.root}/db/#{Rails.env}_structure.sql"`
when 'sqlserver'
- `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql`
+ `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql`
when 'oci', 'oracle'
ActiveRecord::Base.establish_connection(:test)
IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl|
@@ -443,7 +450,7 @@ db_namespace = namespace :db do
case abcs['test']['adapter']
when /mysql/
ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], abcs['test'])
+ ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test']))
when /postgresql/
ActiveRecord::Base.clear_active_connections!
drop_database(abcs['test'])
@@ -452,9 +459,11 @@ db_namespace = namespace :db do
dbfile = abcs['test']['database'] || abcs['test']['dbfile']
File.delete(dbfile) if File.exist?(dbfile)
when 'sqlserver'
- dropfkscript = "#{abcs['test']['host']}.#{abcs['test']['database']}.DP1".gsub(/\\/,'-')
- `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{dropfkscript}`
- `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql`
+ test = abcs.deep_dup['test']
+ test_database = test['database']
+ test['database'] = 'master'
+ ActiveRecord::Base.establish_connection(test)
+ ActiveRecord::Base.connection.recreate_database!(test_database)
when "oci", "oracle"
ActiveRecord::Base.establish_connection(:test)
ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
@@ -480,8 +489,7 @@ db_namespace = namespace :db do
# desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
task :create => :environment do
raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations?
- require 'rails/generators'
- Rails::Generators.configure!
+ Rails.application.load_generators
require 'rails/generators/rails/session_migration/session_migration_generator'
Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ]
end
@@ -498,7 +506,7 @@ namespace :railties do
# desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2"
task :migrations => :'db:load_config' do
to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
- railties = {}
+ railties = ActiveSupport::OrderedHash.new
Rails.application.railties.all do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index bcba85d7a4..5285060288 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
@@ -81,12 +80,6 @@ module ActiveRecord
# Abstract base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
- attr_reader :active_record
-
- def initialize(macro, name, options, active_record)
- @macro, @name, @options, @active_record = macro, name, options, active_record
- end
-
# Returns the name of the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
@@ -105,6 +98,19 @@ module ActiveRecord
# <tt>has_many :clients</tt> returns +{}+
attr_reader :options
+ attr_reader :active_record
+
+ attr_reader :plural_name # :nodoc:
+
+ def initialize(macro, name, options, active_record)
+ @macro = macro
+ @name = name
+ @options = options
+ @active_record = active_record
+ @plural_name = active_record.pluralize_table_names ?
+ name.to_s.pluralize : name.to_s
+ end
+
# Returns the class for the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
@@ -124,7 +130,11 @@ module ActiveRecord
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
- other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ super ||
+ other_aggregation.kind_of?(self.class) &&
+ name == other_aggregation.name &&
+ other_aggregation.options &&
+ active_record == other_aggregation.active_record
end
def sanitized_conditions #:nodoc:
@@ -169,25 +179,8 @@ module ActiveRecord
# Returns a new, unsaved instance of the associated class. +options+ will
# be passed to the class's constructor.
- def build_association(*options)
- klass.new(*options)
- end
-
- # Creates a new instance of the associated class, and immediately saves it
- # with ActiveRecord::Base#save. +options+ will be passed to the class's
- # creation method. Returns the newly created object.
- def create_association(*options)
- klass.create(*options)
- end
-
- # Creates a new instance of the associated class, and immediately saves it
- # with ActiveRecord::Base#save!. +options+ will be passed to the class's
- # creation method. If the created record doesn't pass validations, then an
- # exception will be raised.
- #
- # Returns the newly created object.
- def create_association!(*options)
- klass.create!(*options)
+ def build_association(*options, &block)
+ klass.new(*options, &block)
end
def table_name
@@ -202,17 +195,12 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_name
- foreign_key
- end
- deprecate :primary_key_name => :foreign_key
-
def foreign_type
@foreign_type ||= options[:foreign_type] || "#{name}_type"
end
def type
- @type ||= "#{options[:as]}_type"
+ @type ||= options[:as] && "#{options[:as]}_type"
end
def primary_key_column
@@ -223,22 +211,20 @@ module ActiveRecord
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
- def association_primary_key
- @association_primary_key ||=
- options[:primary_key] ||
- !options[:polymorphic] && klass.primary_key ||
- 'id'
+ # klass option is necessary to support loading polymorphic associations
+ def association_primary_key(klass = nil)
+ options[:primary_key] || primary_key(klass || self.klass)
end
def active_record_primary_key
- @active_record_primary_key ||= options[:primary_key] || active_record.primary_key
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
def counter_cache_column
if options[:counter_cache] == true
"#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
- options[:counter_cache]
+ options[:counter_cache].to_s
end
end
@@ -280,9 +266,7 @@ module ActiveRecord
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
# a hash, array, arel predicate, etc...)
def conditions
- conditions = [options[:conditions]].compact
- conditions << { type => active_record.base_class.name } if options[:as]
- [conditions]
+ [[options[:conditions]].compact]
end
alias :source_macro :macro
@@ -373,14 +357,19 @@ module ActiveRecord
active_record.name.foreign_key
end
end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
end
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
- delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection
+ delegate :foreign_key, :foreign_type, :association_foreign_key,
+ :active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ # Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
@@ -476,17 +465,15 @@ module ActiveRecord
# We want to use the klass from this reflection, rather than just delegate straight to
# the source_reflection, because the source_reflection may be polymorphic. We still
# need to respect the source_reflection's :primary_key option, though.
- def association_primary_key
- @association_primary_key ||= begin
- # Get the "actual" source reflection if the immediate source reflection has a
- # source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || klass.primary_key
+ def association_primary_key(klass = nil)
+ # Get the "actual" source reflection if the immediate source reflection has a
+ # source reflection itself
+ source_reflection = self.source_reflection
+ while source_reflection.source_reflection
+ source_reflection = source_reflection.source_reflection
end
+
+ source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ae9afad48a..ecefaa633c 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/module/delegation'
module ActiveRecord
# = Active Record Relation
@@ -6,13 +7,13 @@ module ActiveRecord
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
# These are explicitly delegated to improve performance (avoids method_missing)
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
- delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass
+ delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
@@ -29,6 +30,7 @@ module ActiveRecord
SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
(ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
@extensions = []
+ @create_with_value = {}
end
def insert(values)
@@ -66,7 +68,7 @@ module ActiveRecord
end
conn.insert(
- im.to_sql,
+ im,
'SQL',
primary_key,
primary_key_value,
@@ -92,6 +94,48 @@ module ActiveRecord
scoping { @klass.create!(*args, &block) }
end
+ # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
+ #
+ # Expects arguments in the same format as <tt>Base.create</tt>.
+ #
+ # ==== Examples
+ # # Find the first user named Penélope or create a new one.
+ # User.where(:first_name => 'Penélope').first_or_create
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ #
+ # # Find the first user named Penélope or create a new one.
+ # # We already have one so the existing record will be returned.
+ # User.where(:first_name => 'Penélope').first_or_create
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ #
+ # # Find the first user named Scarlett or create a new one with a particular last name.
+ # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ #
+ # # Find the first user named Scarlett or create a new one with a different last name.
+ # # We already have one so the existing record will be returned.
+ # User.where(:first_name => 'Scarlett').first_or_create do |user|
+ # user.last_name = "O'Hara"
+ # end
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ def first_or_create(attributes = nil, options = {}, &block)
+ first || create(attributes, options, &block)
+ end
+
+ # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ #
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
+ def first_or_create!(attributes = nil, options = {}, &block)
+ first || create!(attributes, options, &block)
+ end
+
+ # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
+ #
+ # Expects arguments in the same format as <tt>Base.new</tt>.
+ def first_or_initialize(attributes = nil, options = {}, &block)
+ first || new(attributes, options, &block)
+ end
+
def respond_to?(method, include_private = false)
arel.respond_to?(method, include_private) ||
Array.method_defined?(method) ||
@@ -102,24 +146,30 @@ module ActiveRecord
def to_a
return @records if loaded?
- @records = if @readonly_value.nil? && !@klass.locking_enabled?
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
- else
- IdentityMap.without do
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
+ default_scoped = with_default_scope
+
+ if default_scoped.equal?(self)
+ @records = if @readonly_value.nil? && !@klass.locking_enabled?
+ eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
+ else
+ IdentityMap.without do
+ eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
+ end
end
- end
- preload = @preload_values
- preload += @includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
+ preload = @preload_values
+ preload += @includes_values unless eager_loading?
+ preload.each do |associations|
+ ActiveRecord::Associations::Preloader.new(@records, associations).run
+ end
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
- @records.each { |record| record.readonly! } if readonly
+ # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
+ # are JOINS and no explicit SELECT.
+ readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
+ @records.each { |record| record.readonly! } if readonly
+ else
+ @records = default_scoped.to_a
+ end
@loaded = true
@records
@@ -208,19 +258,21 @@ module ActiveRecord
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
- limit = nil
- order = []
- # Apply limit and order only if they're both present
- if @limit_value.present? == @order_values.present?
- limit = arel.limit
- order = arel.orders
- end
+ stmt = Arel::UpdateManager.new(arel.engine)
- stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
- stmt.take limit if limit
- stmt.order(*order)
+ stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.table(table)
stmt.key = table[primary_key]
- @klass.connection.update stmt.to_sql, 'SQL', bind_values
+
+ if joins_values.any?
+ @klass.connection.join_to_update(stmt, arel)
+ else
+ stmt.take(arel.limit)
+ stmt.order(*arel.orders)
+ stmt.wheres = arel.constraints
+ end
+
+ @klass.connection.update stmt, 'SQL', bind_values
end
end
@@ -242,8 +294,7 @@ module ActiveRecord
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
- idx = -1
- id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
+ id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])}
else
object = find(id)
object.update_attributes(attributes)
@@ -287,7 +338,7 @@ module ActiveRecord
end
# Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
- # therefore all callbacks and filters are fired off before the object is deleted. This method is
+ # therefore all callbacks and filters are fired off before the object is deleted. This method is
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
#
# This essentially finds the object (or multiple objects) with the given id, creates a new object
@@ -316,7 +367,7 @@ module ActiveRecord
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
# calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
# goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
- # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
+ # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
# the number of rows affected.
#
# ==== Parameters
@@ -338,8 +389,7 @@ module ActiveRecord
where(conditions).delete_all
else
statement = arel.compile_delete
- affected = @klass.connection.delete(
- statement.to_sql, 'SQL', bind_values)
+ affected = @klass.connection.delete(statement, 'SQL', bind_values)
reset
affected
@@ -385,7 +435,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= arel.to_sql
+ @to_sql ||= klass.connection.to_sql(arel)
end
def where_values_hash
@@ -397,11 +447,21 @@ module ActiveRecord
end
def scope_for_create
- @scope_for_create ||= where_values_hash.merge(@create_with_value || {})
+ @scope_for_create ||= where_values_hash.merge(create_with_value)
end
def eager_loading?
- @should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
+ @should_eager_load ||=
+ @eager_load_values.any? ||
+ @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
+ end
+
+ # Joins that are also marked for preloading. In which case we should just eager load them.
+ # Note that this is a naive implementation because we could have strings and symbols which
+ # represent the same association, but that aren't matched by this. Also, we could have
+ # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] }
+ def joined_includes_values
+ @includes_values & @joins_values
end
def ==(other)
@@ -418,9 +478,10 @@ module ActiveRecord
end
def with_default_scope #:nodoc:
- if default_scoped?
- default_scope = @klass.send(:build_default_scope)
- default_scope ? default_scope.merge(self) : self
+ if default_scoped? && default_scope = klass.send(:build_default_scope)
+ default_scope = default_scope.merge(self)
+ default_scope.default_scoped = false
+ default_scope
else
self
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index d52b84179f..2fd89882ff 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -20,8 +20,6 @@ module ActiveRecord
find_in_batches(options) do |records|
records.each { |record| yield record }
end
-
- self
end
# Yields each batch of records that was found by the find +options+ as
@@ -64,15 +62,18 @@ module ActiveRecord
start = options.delete(:start).to_i
batch_size = options.delete(:batch_size) || 1000
- relation = relation.except(:order).order(batch_order).limit(batch_size)
+ relation = relation.reorder(batch_order).limit(batch_size)
records = relation.where(table[primary_key].gteq(start)).all
while records.any?
+ records_size = records.size
+ primary_key_offset = records.last.id
+
yield records
- break if records.size < batch_size
+ break if records_size < batch_size
- if primary_key_offset = records.last.id
+ if primary_key_offset
records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
else
raise "Primary key not included in the custom select clause"
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index a8a52867ce..af86771d2d 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -66,7 +66,7 @@ module ActiveRecord
calculate(:average, column_name, options)
end
- # Calculates the minimum value on a given column. The value is returned
+ # Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
@@ -89,11 +89,15 @@ module ActiveRecord
# +calculate+ for examples with options.
#
# Person.sum('age') # => 4562
- def sum(column_name, options = {})
- calculate(:sum, column_name, options)
+ def sum(*args)
+ if block_given?
+ self.to_a.sum(*args) {|*block_args| yield(*block_args)}
+ else
+ calculate(:sum, *args)
+ end
end
- # This calculates aggregate values in the given column. Methods for count, sum, average,
+ # This calculates aggregate values in the given column. Methods for count, sum, average,
# minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
# <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
#
@@ -101,7 +105,7 @@ module ActiveRecord
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
# for AVG, and the given column's type for everything else.
# * Grouped values: This returns an ordered hash of the values and groups them by the
- # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
+ # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
#
# values = Person.maximum(:age, :group => 'last_name')
# puts values["Drake"]
@@ -119,7 +123,7 @@ module ActiveRecord
# Options:
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
# See conditions in the intro to ActiveRecord::Base.
- # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
+ # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
# the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
# (Rarely needed).
@@ -146,10 +150,16 @@ module ActiveRecord
if options.except(:distinct).present?
apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct])
else
- if eager_loading? || includes_values.present?
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
+ relation = with_default_scope
+
+ if relation.equal?(self)
+ if eager_loading? || (includes_values.present? && references_eager_loaded_tables?)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
+ else
+ perform_calculation(operation, column_name, options)
+ end
else
- perform_calculation(operation, column_name, options)
+ relation.calculate(operation, column_name, options)
end
end
rescue ThrowResult
@@ -161,21 +171,20 @@ module ActiveRecord
def perform_calculation(operation, column_name, options = {})
operation = operation.to_s.downcase
- distinct = nil
+ distinct = options[:distinct]
if operation == "count"
column_name ||= (select_for_count || :all)
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
- column_name = primary_key if column_name == :all
end
+ column_name = primary_key if column_name == :all && distinct
+
distinct = nil if column_name =~ /\s*DISTINCT\s+/i
end
- distinct = options[:distinct] || distinct
-
if @group_values.any?
execute_grouped_calculation(operation, column_name, distinct)
else
@@ -214,7 +223,7 @@ module ActiveRecord
query_builder = relation.arel
end
- type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
+ type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
@@ -241,6 +250,7 @@ module ActiveRecord
operation,
distinct).as(aggregate_alias)
]
+ select_values += @select_values unless @having_values.empty?
select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
"#{field} AS #{aliaz}"
@@ -249,7 +259,7 @@ module ActiveRecord
relation = except(:group).group(group.join(','))
relation.select_values = select_values
- calculated_data = @klass.connection.select_all(relation.to_sql)
+ calculated_data = @klass.connection.select_all(relation)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 32d1cff6c3..7eeb3dde70 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -83,7 +83,7 @@ module ActiveRecord
#
# Example for find with a lock: Imagine two concurrent transactions:
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
- # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
+ # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
# transaction has to wait until the first is finished; we get the
# expected <tt>person.visits == 4</tt>.
#
@@ -114,7 +114,7 @@ module ActiveRecord
def first(*args)
if args.any?
if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
- to_a.first(*args)
+ limit(*args).to_a
else
apply_finder_options(args.first).first
end
@@ -134,7 +134,11 @@ module ActiveRecord
def last(*args)
if args.any?
if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
- to_a.last(*args)
+ if order_values.empty? && reorder_value.nil?
+ order("#{primary_key} DESC").limit(*args).reverse
+ else
+ to_a.last(*args)
+ end
else
apply_finder_options(args.first).last
end
@@ -180,7 +184,9 @@ module ActiveRecord
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
# Person.exists?
- def exists?(id = nil)
+ def exists?(id = false)
+ return false if id.nil?
+
id = id.id if ActiveRecord::Base === id
join_dependency = construct_join_dependency_for_association_find
@@ -194,7 +200,7 @@ module ActiveRecord
relation = relation.where(table[primary_key].eq(id)) if id
end
- connection.select_value(relation.to_sql) ? true : false
+ connection.select_value(relation, "#{name} Exists") ? true : false
end
protected
@@ -202,7 +208,7 @@ module ActiveRecord
def find_with_associations
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
+ rows = connection.select_all(relation, 'SQL', relation.bind_values)
join_dependency.instantiate(rows)
rescue ThrowResult
[]
@@ -226,7 +232,7 @@ module ActiveRecord
end
def apply_join_dependency(relation, join_dependency)
- for association in join_dependency.join_associations
+ join_dependency.join_associations.each do |association|
relation = association.join_relation(relation)
end
@@ -243,7 +249,7 @@ module ActiveRecord
end
def construct_limited_ids_condition(relation)
- orders = relation.order_values
+ orders = relation.order_values.map { |val| val.presence }.compact
values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
relation = relation.dup
@@ -259,11 +265,13 @@ module ActiveRecord
if match.bang? && result.blank?
raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
else
+ yield(result) if block_given?
result
end
end
def find_or_instantiator_by_attributes(match, attributes, *args)
+ options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
args.each_with_index do |arg, i|
if arg.is_a?(Hash)
@@ -278,8 +286,7 @@ module ActiveRecord
record = where(conditions).first
unless record
- record = @klass.new do |r|
- r.assign_attributes(protected_attributes_for_create)
+ record = @klass.new(protected_attributes_for_create, options) do |r|
r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
end
yield(record) if block_given?
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 2814771002..7e8ddd1b5d 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -19,7 +19,7 @@ module ActiveRecord
case value
when ActiveRecord::Relation
- value.select_values = [value.klass.arel_table['id']] if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map { |x|
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 94aa999715..670ba0987d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -9,7 +9,7 @@ module ActiveRecord
:select_values, :group_values, :order_values, :joins_values,
:where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
- :from_value, :reorder_value
+ :from_value, :reorder_value, :reverse_order_value
def includes(*args)
args.reject! {|a| a.blank? }
@@ -37,6 +37,35 @@ module ActiveRecord
relation
end
+ # Works in two unique ways.
+ #
+ # First: takes a block so it can be used just like Array#select.
+ #
+ # Model.scoped.select { |m| m.field == value }
+ #
+ # This will build an array of objects from the database for the scope,
+ # converting them into an array and iterating through them using Array#select.
+ #
+ # Second: Modifies the SELECT statement for the query so that only certain
+ # fields are retrieved:
+ #
+ # >> Model.select(:field)
+ # => [#<Model field:value>]
+ #
+ # Although in the above example it looks as though this method returns an
+ # array, it actually returns a relation object and can have other query
+ # methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
+ #
+ # This method will also take multiple parameters:
+ #
+ # >> Model.select(:field, :other_field, :and_one_more)
+ # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+ #
+ # Any attributes that do not have fields retrieved by a select
+ # will return `nil` when the getter method for that attribute is used:
+ #
+ # >> Model.select(:field).first.other_field
+ # => nil
def select(value = Proc.new)
if block_given?
to_a.select {|*block_args| value.call(*block_args) }
@@ -96,11 +125,11 @@ module ActiveRecord
relation
end
- def having(*args)
- return self if args.blank?
+ def having(opts, *rest)
+ return self if opts.blank?
relation = clone
- relation.having_values += build_where(*args)
+ relation.having_values += build_where(opts, rest)
relation
end
@@ -137,7 +166,7 @@ module ActiveRecord
def create_with(value)
relation = clone
- relation.create_with_value = value && (@create_with_value || {}).merge(value)
+ relation.create_with_value = value ? create_with_value.merge(value) : {}
relation
end
@@ -147,6 +176,42 @@ module ActiveRecord
relation
end
+ # Used to extend a scope with additional methods, either through
+ # a module or through a block provided.
+ #
+ # The object returned is a relation, which can be further extended.
+ #
+ # === Using a module
+ #
+ # module Pagination
+ # def page(number)
+ # # pagination code goes here
+ # end
+ # end
+ #
+ # scope = Model.scoped.extending(Pagination)
+ # scope.page(params[:page])
+ #
+ # You can also pass a list of modules:
+ #
+ # scope = Model.scoped.extending(Pagination, SomethingElse)
+ #
+ # === Using a block
+ #
+ # scope = Model.scoped.extending do
+ # def page(number)
+ # # pagination code goes here
+ # end
+ # end
+ # scope.page(params[:page])
+ #
+ # You can also use a block and a module list:
+ #
+ # scope = Model.scoped.extending(Pagination) do
+ # def per_page(number)
+ # # pagination code goes here
+ # end
+ # end
def extending(*modules)
modules << Module.new(&Proc.new) if block_given?
@@ -158,13 +223,9 @@ module ActiveRecord
end
def reverse_order
- order_clause = arel.order_clauses
-
- order = order_clause.empty? ?
- "#{table_name}.#{primary_key} DESC" :
- reverse_sql_order(order_clause).join(', ')
-
- except(:order).order(Arel.sql(order))
+ relation = clone
+ relation.reverse_order_value = !relation.reverse_order_value
+ relation
end
def arel
@@ -186,6 +247,7 @@ module ActiveRecord
arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
order = @reorder_value ? @reorder_value : @order_values
+ order = reverse_sql_order(order) if @reverse_order_value
arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
build_select(arel, @select_values.uniq)
@@ -257,12 +319,12 @@ module ActiveRecord
association_joins = buckets['association_join'] || []
stashed_association_joins = buckets['stashed_join'] || []
- join_nodes = buckets['join_node'] || []
+ join_nodes = (buckets['join_node'] || []).uniq
string_joins = (buckets['string_join'] || []).map { |x|
x.strip
}.uniq
- join_list = custom_join_ast(manager, string_joins)
+ join_list = join_nodes + custom_join_ast(manager, string_joins)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
@@ -270,10 +332,6 @@ module ActiveRecord
join_list
)
- join_nodes.each do |join|
- join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
- end
-
join_dependency.graft(*stashed_association_joins)
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
@@ -283,7 +341,6 @@ module ActiveRecord
association.join_to(manager)
end
- manager.join_sources.concat join_nodes.uniq
manager.join_sources.concat join_list
manager
@@ -306,9 +363,21 @@ module ActiveRecord
end
def reverse_sql_order(order_query)
- order_query.join(', ').split(',').collect do |s|
- s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
- end
+ order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
+
+ order_query.map do |o|
+ case o
+ when Arel::Nodes::Ordering
+ o.reverse
+ when String, Symbol
+ o.to_s.split(',').collect do |s|
+ s.strip!
+ s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
+ end
+ else
+ o
+ end
+ end.flatten
end
def array_of_strings?(o)
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 69706b5ead..ba882beca9 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -55,7 +55,7 @@ module ActiveRecord
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
- merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value
+ merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty?
# Apply scope extension modules
merged_relation.send :apply_modules, r.extensions
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 243012f88c..9ceab2eabc 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -1,7 +1,7 @@
module ActiveRecord
###
# This class encapsulates a Result returned from calling +exec_query+ on any
- # database connection adapter. For example:
+ # database connection adapter. For example:
#
# x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
# x # => #<ActiveRecord::Result:0xdeadbeef>
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index a893c0ad85..6fe305f843 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -40,6 +40,10 @@ module ActiveRecord
def header(stream)
define_params = @version ? ":version => #{@version}" : ""
+ if stream.respond_to?(:external_encoding)
+ stream.puts "# encoding: #{stream.external_encoding.name}"
+ end
+
stream.puts <<HEADER
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
@@ -106,7 +110,7 @@ HEADER
spec = {}
spec[:name] = column.name.inspect
- # AR has an optimisation which handles zero-scale decimals as integers. This
+ # AR has an optimization which handles zero-scale decimals as integers. This
# code ensures that the dumper still dumps the column as a decimal.
spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) }
'decimal'
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 2bde06f562..5ad40d8cd9 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -10,50 +10,8 @@ module ActiveRecord #:nodoc:
options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
options[:except] |= Array.wrap(self.class.inheritance_column)
- hash = super(options)
-
- serializable_add_includes(options) do |association, records, opts|
- hash[association] = records.is_a?(Enumerable) ?
- records.map { |r| r.serializable_hash(opts) } :
- records.serializable_hash(opts)
- end
-
- hash
+ super(options)
end
-
- private
- # Add associations specified via the <tt>:include</tt> option.
- #
- # Expects a block that takes as arguments:
- # +association+ - name of the association
- # +records+ - the association record(s) to be serialized
- # +opts+ - options for the association records
- def serializable_add_includes(options = {})
- return unless include_associations = options.delete(:include)
-
- base_only_or_except = { :except => options[:except],
- :only => options[:only] }
-
- include_has_options = include_associations.is_a?(Hash)
- associations = include_has_options ? include_associations.keys : Array.wrap(include_associations)
-
- for association in associations
- records = case self.class.reflect_on_association(association).macro
- when :has_many, :has_and_belongs_to_many
- send(association).to_a
- when :has_one, :belongs_to
- send(association)
- end
-
- if records
- association_options = include_has_options ? include_associations[association] : base_only_or_except
- opts = options.merge(association_options)
- yield(association, records, opts)
- end
- end
-
- options[:include] = include_associations
- end
end
end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 8c4adf7116..0e7f57aa43 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -75,7 +75,7 @@ module ActiveRecord #:nodoc:
# </firm>
#
# Additionally, the record being serialized will be passed to a Proc's second
- # parameter. This allows for ad hoc additions to the resultant document that
+ # parameter. This allows for ad hoc additions to the resultant document that
# incorporate the context of the record being serialized. And by leveraging the
# closure created by a Proc, to_xml can be used to add elements that normally fall
# outside of the scope of the model -- for example, generating and appending URLs
@@ -179,49 +179,7 @@ module ActiveRecord #:nodoc:
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
def initialize(*args)
super
- options[:except] |= Array.wrap(@serializable.class.inheritance_column)
- end
-
- def add_extra_behavior
- add_includes
- end
-
- def add_includes
- procs = options.delete(:procs)
- @serializable.send(:serializable_add_includes, options) do |association, records, opts|
- add_associations(association, records, opts)
- end
- options[:procs] = procs
- end
-
- # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
- def add_associations(association, records, opts)
- association_name = association.to_s.singularize
- merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true)
-
- if records.is_a?(Enumerable)
- tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
- type = options[:skip_types] ? { } : {:type => "array"}
-
- if records.empty?
- @builder.tag!(tag, type)
- else
- @builder.tag!(tag, type) do
- records.each do |record|
- if options[:skip_types]
- record_type = {}
- else
- record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
- record_type = {:type => record_class}
- end
-
- record.to_xml merged_options.merge(record_type)
- end
- end
- end
- elsif record = @serializable.send(association)
- record.to_xml(merged_options)
- end
+ options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column)
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 98e21db908..76c37cc367 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# = Active Record Session Store
#
- # A session store backed by an Active Record class. A default class is
+ # A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record Session class
# with text +session_id+ and +data+ attributes is sufficient.
#
@@ -23,7 +23,7 @@ module ActiveRecord
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
#
# Note that setting the primary key to the +session_id+ frees you from
- # having a separate +id+ column if you don't want it. However, you must
+ # having a separate +id+ column if you don't want it. However, you must
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
# on ApplicationController is a good place.
#
@@ -40,13 +40,13 @@ module ActiveRecord
# You must implement these methods:
#
# self.find_by_session_id(session_id)
- # initialize(hash_of_session_id_and_data)
+ # initialize(hash_of_session_id_and_data, options_hash = {})
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
- # The example SqlBypass class is a generic SQL session store. You may
+ # The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class SessionStore < ActionDispatch::Session::AbstractStore
module ClassMethods # :nodoc:
@@ -79,7 +79,7 @@ module ActiveRecord
##
# :singleton-method:
- # Customizable data column name. Defaults to 'data'.
+ # Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
@@ -125,7 +125,7 @@ module ActiveRecord
end
end
- def initialize(attributes = nil)
+ def initialize(attributes = nil, options = {})
@data = nil
super
end
@@ -161,12 +161,12 @@ module ActiveRecord
end
# A barebones session store which duck-types with the default session
- # store but bypasses Active Record and issues SQL directly. This is
+ # store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
- # are configurable class attributes. Marshaling and unmarshaling
- # are implemented as class methods that you may override. By default,
+ # are configurable class attributes. Marshaling and unmarshaling
+ # are implemented as class methods that you may override. By default,
# marshaling data is
#
# ActiveSupport::Base64.encode64(Marshal.dump(data))
@@ -176,18 +176,13 @@ module ActiveRecord
# Marshal.load(ActiveSupport::Base64.decode64(data))
#
# This marshaling behavior is intended to store the widest range of
- # binary session data in a +text+ column. For higher performance,
+ # binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
extend ClassMethods
##
# :singleton-method:
- # Use the ActiveRecord::Base.connection by default.
- cattr_accessor :connection
-
- ##
- # :singleton-method:
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
@@ -206,10 +201,19 @@ module ActiveRecord
class << self
alias :data_column_name :data_column
+
+ # Use the ActiveRecord::Base.connection by default.
+ attr_writer :connection
+
+ # Use the ActiveRecord::Base.connection_pool by default.
+ attr_writer :connection_pool
- remove_method :connection
def connection
- @@connection ||= ActiveRecord::Base.connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def connection_pool
+ @connection_pool ||= ActiveRecord::Base.connection_pool
end
# Look up a session by id and unmarshal its data if found.
@@ -219,6 +223,8 @@ module ActiveRecord
end
end
end
+
+ delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self
attr_reader :session_id, :new_record
alias :new_record? :new_record
@@ -286,7 +292,7 @@ module ActiveRecord
end
end
- # The class used for session storage. Defaults to
+ # The class used for session storage. Defaults to
# ActiveRecord::SessionStore::Session
cattr_accessor :session_class
self.session_class = Session
@@ -297,8 +303,12 @@ module ActiveRecord
private
def get_session(env, sid)
Base.silence do
- sid ||= generate_sid
- session = find_session(sid)
+ unless sid and session = @@session_class.find_by_session_id(sid)
+ # If the sid was nil or if there is no pre-existing session under the sid,
+ # force the generation of a new sid and associate a new session associated with the new sid
+ sid = generate_sid
+ session = @@session_class.new(:session_id => sid, :data => {})
+ end
env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 29efbbcb8c..ffe9b08dce 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -13,6 +13,13 @@ module ActiveRecord
ActiveRecord::IdentityMap.clear
end
+ # Backport skip to Ruby 1.8. test/unit doesn't support it, so just
+ # make it a noop.
+ unless instance_methods.map(&:to_s).include?("skip")
+ def skip(message)
+ end
+ end
+
def assert_date_from_db(expected, actual, message = nil)
# SybaseAdapter doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
@@ -24,27 +31,30 @@ module ActiveRecord
end
def assert_sql(*patterns_to_match)
- $queries_executed = []
+ ActiveRecord::SQLCounter.log = []
yield
- $queries_executed
+ ActiveRecord::SQLCounter.log
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
- failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
+ failed_patterns << pattern unless ActiveRecord::SQLCounter.log.any?{ |sql| pattern === sql }
end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{ActiveRecord::SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{ActiveRecord::SQLCounter.log.join("\n")}"}"
end
def assert_queries(num = 1)
- $queries_executed = []
+ ActiveRecord::SQLCounter.log = []
yield
ensure
- %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
- assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
+ assert_equal num, ActiveRecord::SQLCounter.log.size, "#{ActiveRecord::SQLCounter.log.size} instead of #{num} queries were executed.#{ActiveRecord::SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{ActiveRecord::SQLCounter.log.join("\n")}"}"
end
def assert_no_queries(&block)
+ prev_ignored_sql = ActiveRecord::SQLCounter.ignored_sql
+ ActiveRecord::SQLCounter.ignored_sql = []
assert_queries(0, &block)
+ ensure
+ ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql
end
def with_kcode(kcode)
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 1511c71ffc..4d5e469a7f 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -37,6 +37,10 @@ module ActiveRecord
self.record_timestamps = true
end
+ def initialize_dup(other)
+ clear_timestamp_attributes
+ end
+
private
def create #:nodoc:
@@ -44,7 +48,9 @@ module ActiveRecord
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil?
+ if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
+ write_attribute(column.to_s, current_time)
+ end
end
end
@@ -95,6 +101,13 @@ module ActiveRecord
def current_time_from_proper_timezone #:nodoc:
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
+
+ # Clear attributes and changed_attributes
+ def clear_timestamp_attributes
+ all_timestamp_attributes_in_model.each do |attribute_name|
+ self[attribute_name] = nil
+ changed_attributes.delete(attribute_name)
+ end
+ end
end
end
-
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index d363f36108..ae97a3f3ca 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -165,7 +165,7 @@ module ActiveRecord
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints on MySQL and PostgreSQL. See
- # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
# for more information about savepoints.
#
# === Callbacks
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index de36dd20b3..4b075183c3 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# = Active Record RecordInvalid
#
- # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
# +record+ method to retrieve the record which did not validate.
#
# begin
@@ -32,11 +32,11 @@ module ActiveRecord
module ClassMethods
# Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
# so an exception is raised if the record is invalid.
- def create!(attributes = nil, &block)
+ def create!(attributes = nil, options = {}, &block)
if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
+ attributes.collect { |attr| create!(attr, options, &block) }
else
- object = new(attributes)
+ object = new(attributes, options)
yield(object) if block_given?
object.save!
object
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 3a783aeb00..7af0352a31 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -17,15 +17,7 @@ module ActiveRecord
# validates_associated :pages, :library
# end
#
- # Warning: If, after the above definition, you then wrote:
- #
- # class Page < ActiveRecord::Base
- # belongs_to :book
- #
- # validates_associated :book
- # end
- #
- # this would specify a circular dependency and cause infinite recursion.
+ # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
#
# NOTE: This validation will not fail if the association hasn't been assigned. If you want to
# ensure that the association is both present and guaranteed to be valid, you also need to
@@ -37,10 +29,10 @@ module ActiveRecord
# validation contexts by default (+nil+), other options are <tt>:create</tt>
# and <tt>:update</tt>.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 4db4105389..484b1d369b 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -83,7 +83,7 @@ module ActiveRecord
# validates_uniqueness_of :user_name, :scope => :account_id
# end
#
- # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once
+ # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once
# per semester for a particular class.
#
# class TeacherSchedule < ActiveRecord::Base
@@ -105,7 +105,7 @@ module ActiveRecord
# The method, proc or string should return or evaluate to a true or false value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or
- # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
# return or evaluate to a true or false value.
#
# === Concurrency and integrity
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 2c20dd997f..838aa8fb1e 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,9 +1,9 @@
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 3
- MINOR = 1
+ MINOR = 2
TINY = 0
- PRE = "beta1"
+ PRE = "beta"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
index 4f81a52fd0..851930344a 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
@@ -1,7 +1,7 @@
class <%= migration_class_name %> < ActiveRecord::Migration
def change
create_table :<%= table_name %> do |t|
-<% for attribute in attributes -%>
+<% attributes.each do |attribute| -%>
t.<%= attribute.type %> :<%= attribute.name %>
<% end -%>
<% if options[:timestamps] %>
diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb
index 8f0bf1ef0d..9ea3248513 100644
--- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb
@@ -1,5 +1,5 @@
class <%= migration_class_name %> < ActiveRecord::Migration
- def up
+ def change
create_table :<%= session_table_name %> do |t|
t.string :session_id, :null => false
t.text :data
@@ -9,8 +9,4 @@ class <%= migration_class_name %> < ActiveRecord::Migration
add_index :<%= session_table_name %>, :session_id
add_index :<%= session_table_name %>, :updated_at
end
-
- def down
- drop_table :<%= session_table_name %>
- end
end
diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore
new file mode 100644
index 0000000000..a0ec5967dd
--- /dev/null
+++ b/activerecord/test/.gitignore
@@ -0,0 +1 @@
+/config.yml
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 49b2e945c3..94497e37c7 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -43,7 +43,7 @@ class AdapterTest < ActiveRecord::TestCase
def test_current_database
if @connection.respond_to?(:current_database)
- assert_equal ENV['ARUNIT_DB_NAME'] || "activerecord_unittest", @connection.current_database
+ assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database
end
end
@@ -68,7 +68,12 @@ class AdapterTest < ActiveRecord::TestCase
begin
assert_nothing_raised do
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
- ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses"
+
+ config = ARTest.connection_config
+ ActiveRecord::Base.connection.execute(
+ "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
+ "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
+ )
end
ensure
ActiveRecord::Base.establish_connection 'arunit'
@@ -76,12 +81,6 @@ class AdapterTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:PostgreSQLAdapter)
- def test_encoding
- assert_not_nil @connection.encoding
- end
- end
-
def test_table_alias
def @connection.test_table_alias_length() 10; end
class << @connection
@@ -141,4 +140,31 @@ class AdapterTest < ActiveRecord::TestCase
end
end
end
+
+ def test_disable_referential_integrity
+ assert_nothing_raised do
+ @connection.disable_referential_integrity do
+ # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
+ if @connection.prefetch_primary_key?
+ id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
+ @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
+ else
+ @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
+ end
+ # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block
+ # and will fail (at least on Oracle)
+ @connection.execute "DELETE FROM fk_test_has_fk"
+ end
+ end
+ end
+
+ def test_deprecated_visitor_for
+ visitor_klass = Class.new(Arel::Visitors::ToSql)
+ Arel::Visitors::VISITORS['fuuu'] = visitor_klass
+ pool = stub(:spec => stub(:config => { :adapter => 'fuuu' }))
+ visitor = assert_deprecated {
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.visitor_for(pool)
+ }
+ assert visitor.is_a?(visitor_klass)
+ end
end
diff --git a/activerecord/test/cases/adapters/firebird/migration_test.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb
index 710661b9bd..5c94593765 100644
--- a/activerecord/test/cases/adapters/firebird/migration_test.rb
+++ b/activerecord/test/cases/adapters/firebird/migration_test.rb
@@ -24,7 +24,7 @@ class FirebirdMigrationTest < ActiveRecord::TestCase
assert !sequence_exists?('foo_seq')
assert sequence_exists?('foo_custom_seq')
- assert_nothing_raised { @connection.drop_table(:foo, :sequence => 'foo_custom_seq') }
+ assert_nothing_raised { @connection.drop_table(:foo) }
assert !sequence_exists?('foo_custom_seq')
ensure
FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 509baacaef..94fc3564df 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
- ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
alias_method :execute_without_stub, :execute
remove_method :execute
def execute(sql, name = nil) return sql end
@@ -10,7 +10,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
def teardown
- ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_without_stub
end
@@ -99,7 +99,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
#we need to actually modify some data, so we make execute point to the original method
- ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
@@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
yield
ensure
#before finishing, we restore the alias to the mock-up method
- ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index eee771ecff..2a89430da9 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -13,6 +13,16 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_connect_with_url
+ run_without_connection do |orig|
+ ar_config = ARTest.connection_config['arunit']
+ url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
+ klass = Class.new(ActiveRecord::Base)
+ klass.establish_connection(url)
+ assert_equal ar_config['database'], klass.connection.current_database
+ end
+ end
+
def test_mysql_reconnect_attribute_after_connection_with_reconnect_false
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false}))
diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
index 9673e2bb46..3d1330efb8 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -23,4 +23,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
new file mode 100644
index 0000000000..83de90f179
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -0,0 +1,23 @@
+require 'cases/helper'
+
+module ActiveRecord::ConnectionAdapters
+ class MysqlAdapter
+ class StatementPoolTest < ActiveRecord::TestCase
+ def test_cache_is_per_pid
+ return skip('must support fork') unless Process.respond_to?(:fork)
+
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
+
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
+
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index 752b864818..3a9744e78f 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -87,8 +87,8 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { x.save }
x.order = 'y'
assert_nothing_raised { x.save }
- assert_nothing_raised { y = Group.find_by_order('y') }
- assert_nothing_raised { y = Group.find(1) }
+ assert_nothing_raised { Group.find_by_order('y') }
+ assert_nothing_raised { Group.find(1) }
x = Group.find(1)
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
new file mode 100644
index 0000000000..21b97b3b39
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+
+module ActiveRecord
+ class PostgresqlConnectionTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_encoding
+ assert_not_nil @connection.encoding
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 7c49236854..d57794daf8 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -10,6 +10,45 @@ module ActiveRecord
@connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
end
+ def test_primary_key
+ assert_equal 'id', @connection.primary_key('ex')
+ end
+
+ def test_non_standard_primary_key
+ @connection.exec_query('drop table if exists ex')
+ @connection.exec_query('create table ex(data character varying(255) primary key)')
+ assert_equal 'data', @connection.primary_key('ex')
+ end
+
+ def test_primary_key_returns_nil_for_no_pk
+ @connection.exec_query('drop table if exists ex')
+ @connection.exec_query('create table ex(id integer)')
+ assert_nil @connection.primary_key('ex')
+ end
+
+ def test_primary_key_raises_error_if_table_not_found
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.primary_key('unobtainium')
+ end
+ end
+
+ def test_insert_sql_with_proprietary_returning_clause
+ id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
+ assert_equal "5150", id
+ end
+
+ def test_insert_sql_with_quoted_schema_and_table_name
+ id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
+
+ def test_insert_sql_with_no_space_after_table_name
+ id = @connection.insert_sql("insert into ex(number) values(5150)")
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
+
def test_serial_sequence
assert_equal 'public.accounts_id_seq',
@connection.serial_sequence('accounts', 'id')
@@ -35,6 +74,36 @@ module ActiveRecord
@connection.default_sequence_name('zomg')
end
+ def test_pk_and_sequence_for
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @connection.default_sequence_name('ex', 'id'), seq
+ end
+
+ def test_pk_and_sequence_for_with_non_standard_primary_key
+ @connection.exec_query('drop table if exists ex')
+ @connection.exec_query('create table ex(code serial primary key)')
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'code', pk
+ assert_equal @connection.default_sequence_name('ex', 'code'), seq
+ end
+
+ def test_pk_and_sequence_for_returns_nil_if_no_seq
+ @connection.exec_query('drop table if exists ex')
+ @connection.exec_query('create table ex(id integer primary key)')
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
+
+ def test_pk_and_sequence_for_returns_nil_if_no_pk
+ @connection.exec_query('drop table if exists ex')
+ @connection.exec_query('create table ex(id integer)')
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
+
+ def test_pk_and_sequence_for_returns_nil_if_table_not_found
+ assert_nil @connection.pk_and_sequence_for('unobtainium')
+ end
+
def test_exec_insert_number
insert(@connection, 'number' => 10)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index a5c3e69af9..b01eabc840 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -20,6 +20,7 @@ class SchemaTest < ActiveRecord::TestCase
'email character varying(50)',
'moment timestamp without time zone default now()'
]
+ PK_TABLE_NAME = 'table_with_pk'
class Thing1 < ActiveRecord::Base
set_table_name "test_schema.things"
@@ -37,6 +38,10 @@ class SchemaTest < ActiveRecord::TestCase
set_table_name 'test_schema."Things"'
end
+ class Thing5 < ActiveRecord::Base
+ set_table_name 'things'
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@@ -49,6 +54,7 @@ class SchemaTest < ActiveRecord::TestCase
@connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});"
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
+ @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
end
def teardown
@@ -63,12 +69,36 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_table_exists_when_on_schema_search_path
+ with_schema_search_path(SCHEMA_NAME) do
+ assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found")
+ end
+ end
+
+ def test_table_exists_when_not_on_schema_search_path
+ with_schema_search_path('PUBLIC') do
+ assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found")
+ end
+ end
+
def test_table_exists_wrong_schema
assert(!@connection.table_exists?("foo.things"), "table should not exist")
end
+ def test_table_exists_quoted_names
+ [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given|
+ assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ end
+ with_schema_search_path(SCHEMA_NAME) do
+ given = %("#{TABLE_NAME}")
+ assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ end
+ end
+
def test_table_exists_quoted_table
- assert(@connection.table_exists?('"things.table"'), "table should exist")
+ with_schema_search_path(SCHEMA_NAME) do
+ assert(@connection.table_exists?('"things.table"'), "table should exist")
+ end
end
def test_with_schema_prefixed_table_name
@@ -91,7 +121,6 @@ class SchemaTest < ActiveRecord::TestCase
end
end
-
def test_proper_encoding_of_table_name
assert_equal '"table_name"', @connection.quote_table_name('table_name')
assert_equal '"table.name"', @connection.quote_table_name('"table.name"')
@@ -164,6 +193,79 @@ class SchemaTest < ActiveRecord::TestCase
ActiveRecord::Base.connection.schema_search_path = "public"
end
+ def test_primary_key_with_schema_specified
+ [
+ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
+ %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"),
+ %(#{SCHEMA_NAME}.#{PK_TABLE_NAME})
+ ].each do |given|
+ assert_equal 'id', @connection.primary_key(given), "primary key should be found when table referenced as #{given}"
+ end
+ end
+
+ def test_primary_key_assuming_schema_search_path
+ with_schema_search_path(SCHEMA_NAME) do
+ assert_equal 'id', @connection.primary_key(PK_TABLE_NAME), "primary key should be found"
+ end
+ end
+
+ def test_primary_key_raises_error_if_table_not_found_on_schema_search_path
+ with_schema_search_path(SCHEMA2_NAME) do
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.primary_key(PK_TABLE_NAME)
+ end
+ end
+ end
+
+ def test_pk_and_sequence_for_with_schema_specified
+ [
+ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
+ %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"),
+ %(#{SCHEMA_NAME}.#{PK_TABLE_NAME})
+ ].each do |given|
+ pk, seq = @connection.pk_and_sequence_for(given)
+ assert_equal 'id', pk, "primary key should be found when table referenced as #{given}"
+ assert_equal "#{SCHEMA_NAME}.#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}"
+ end
+ end
+
+ def test_current_schema
+ {
+ %('$user',public) => 'public',
+ SCHEMA_NAME => SCHEMA_NAME,
+ %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME,
+ %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => 'public'
+ }.each do |given,expect|
+ with_schema_search_path(given) { assert_equal expect, @connection.current_schema }
+ end
+ end
+
+ def test_prepared_statements_with_multiple_schemas
+
+ @connection.schema_search_path = SCHEMA_NAME
+ Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
+
+ @connection.schema_search_path = SCHEMA2_NAME
+ Thing5.create(:id => 1, :name => "thing inside #{SCHEMA2_NAME}", :email => "thing1@localhost", :moment => Time.now)
+
+ @connection.schema_search_path = SCHEMA_NAME
+ assert_equal 1, Thing5.count
+
+ @connection.schema_search_path = SCHEMA2_NAME
+ assert_equal 1, Thing5.count
+ end
+
+ def test_schema_exists?
+ {
+ 'public' => true,
+ SCHEMA_NAME => true,
+ SCHEMA2_NAME => true,
+ 'darkside' => false
+ }.each do |given,expect|
+ assert_equal expect, @connection.schema_exists?(given)
+ end
+ end
+
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
new file mode 100644
index 0000000000..a82c6f67d6
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -0,0 +1,23 @@
+require 'cases/helper'
+
+module ActiveRecord::ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class StatementPoolTest < ActiveRecord::TestCase
+ def test_cache_is_per_pid
+ return skip('must support fork') unless Process.respond_to?(:fork)
+
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
+
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
+
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
new file mode 100644
index 0000000000..337f43c421
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -0,0 +1,30 @@
+require 'cases/helper'
+require 'models/developer'
+
+class TimestampTest < ActiveRecord::TestCase
+ def test_load_infinity_and_beyond
+ unless current_adapter?(:PostgreSQLAdapter)
+ return skip("only tested on postgresql")
+ end
+
+ d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at")
+ assert d.first.updated_at.infinite?, 'timestamp should be infinite'
+
+ d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at")
+ time = d.first.updated_at
+ assert time.infinite?, 'timestamp should be infinite'
+ assert_operator time, :<, 0
+ end
+
+ def test_save_infinity_and_beyond
+ unless current_adapter?(:PostgreSQLAdapter)
+ return skip("only tested on postgresql")
+ end
+
+ d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0)
+ assert_equal(1.0 / 0.0, d.updated_at)
+
+ d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0)
+ assert_equal(-1.0 / 0.0, d.updated_at)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
new file mode 100644
index 0000000000..5f08f79171
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -0,0 +1,18 @@
+class PostgreSQLUtilsTest < ActiveSupport::TestCase
+ include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils
+
+ def test_extract_schema_and_table
+ {
+ %(table_name) => [nil,'table_name'],
+ %("table.name") => [nil,'table.name'],
+ %(schema.table_name) => %w{schema table_name},
+ %("schema".table_name) => %w{schema table_name},
+ %(schema."table_name") => %w{schema table_name},
+ %("schema"."table_name") => %w{schema table_name},
+ %("even spaces".table) => ['even spaces','table'],
+ %(schema."table.name") => ['schema', 'table.name']
+ }.each do |given, expect|
+ assert_equal expect, extract_schema_and_table(given)
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
new file mode 100644
index 0000000000..303ba9245a
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/view_test.rb
@@ -0,0 +1,49 @@
+require "cases/helper"
+
+class ViewTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ SCHEMA_NAME = 'test_schema'
+ TABLE_NAME = 'things'
+ VIEW_NAME = 'view_things'
+ COLUMNS = [
+ 'id integer',
+ 'name character varying(50)',
+ 'email character varying(50)',
+ 'moment timestamp without time zone'
+ ]
+
+ class ThingView < ActiveRecord::Base
+ set_table_name 'test_schema.view_things'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
+ @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
+ @connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
+ end
+
+ def teardown
+ @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ end
+
+ def test_table_exists
+ name = ThingView.table_name
+ assert @connection.table_exists?(name), "'#{name}' table should exist"
+ end
+
+ def test_column_definitions
+ assert_nothing_raised do
+ assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}")
+ end
+ end
+
+ private
+ def columns(table_name)
+ @connection.send(:column_definitions, table_name).map do |name, type, default|
+ "#{name} #{type}" + (default ? " default #{default}" : '')
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 0d9db92447..e0152e7ccf 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -67,7 +67,7 @@ module ActiveRecord
def test_type_cast_bigdecimal
bd = BigDecimal.new '10.0'
- assert_equal bd.to_s('F'), @conn.type_cast(bd, nil)
+ assert_equal bd.to_f, @conn.type_cast(bd, nil)
end
def test_type_cast_unknown
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 6ff04e3eb3..eb6f071dc1 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,9 +1,12 @@
# encoding: utf-8
require "cases/helper"
+require 'models/owner'
module ActiveRecord
module ConnectionAdapters
class SQLite3AdapterTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
class DualEncoding < ActiveRecord::Base
end
@@ -19,6 +22,21 @@ module ActiveRecord
eosql
end
+ def test_column_types
+ return skip('only test encoding on 1.9') unless "<3".encoding_aware?
+
+ owner = Owner.create!(:name => "hello".encode('ascii-8bit'))
+ owner.reload
+ select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', '
+ result = Owner.connection.exec_query <<-esql
+ SELECT #{select}
+ FROM #{Owner.table_name}
+ WHERE #{Owner.primary_key} = #{owner.id}
+ esql
+
+ assert(!result.rows.first.include?("blob"), "should not store blobs")
+ end
+
def test_exec_insert
column = @conn.columns('items').find { |col| col.name == 'number' }
vals = [[column, 10]]
@@ -139,6 +157,8 @@ module ActiveRecord
binary = DualEncoding.new :name => 'いただきます!', :data => str
binary.save!
assert_equal str, binary.data
+
+ DualEncoding.connection.drop_table('dual_encodings')
end
def test_execute
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
new file mode 100644
index 0000000000..ae272e2c4b
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -0,0 +1,24 @@
+require 'cases/helper'
+
+module ActiveRecord::ConnectionAdapters
+ class SQLiteAdapter
+ class StatementPoolTest < ActiveRecord::TestCase
+ def test_cache_is_per_pid
+ return skip('must support fork') unless Process.respond_to?(:fork)
+
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
+
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
+
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
+ end
+end
+
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index ddcc36c841..1160d236c9 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -13,6 +13,7 @@ require 'models/comment'
require 'models/sponsor'
require 'models/member'
require 'models/essay'
+require 'models/toy'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -158,6 +159,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
end
+ def test_polymorphic_association_class
+ sponsor = Sponsor.new
+ assert_nil sponsor.association(:sponsorable).send(:klass)
+
+ sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL
+ assert_nil sponsor.association(:sponsorable).send(:klass)
+
+ sponsor.sponsorable = Member.new :name => "Bert"
+ assert_equal Member, sponsor.association(:sponsorable).send(:klass)
+ end
+
def test_with_polymorphic_and_condition
sponsor = Sponsor.create
member = Member.create :name => "Bert"
@@ -285,6 +297,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
+ def test_belongs_to_counter_when_update_column
+ topic = Topic.create!(:title => "37s")
+ topic.replies.create!(:title => "re: 37s", :content => "rails")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.update_column(:content, "rails is wonderfull")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
@@ -332,6 +353,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal members(:groucho), sponsor.sponsorable
end
+ def test_dont_find_target_when_foreign_key_is_null
+ tagging = taggings(:thinking_general)
+ queries = assert_sql { tagging.super_tag }
+ assert_equal 0, queries.length
+ end
+
def test_field_name_same_as_foreign_key
computer = Computer.find(1)
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
@@ -626,4 +653,55 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal "Bob", firm.name
end
+
+ def test_build_with_block
+ client = Client.create(:name => 'Client Company')
+
+ firm = client.build_firm{ |f| f.name = 'Agency Company' }
+ assert_equal 'Agency Company', firm.name
+ end
+
+ def test_create_with_block
+ client = Client.create(:name => 'Client Company')
+
+ firm = client.create_firm{ |f| f.name = 'Agency Company' }
+ assert_equal 'Agency Company', firm.name
+ end
+
+ def test_create_bang_with_block
+ client = Client.create(:name => 'Client Company')
+
+ firm = client.create_firm!{ |f| f.name = 'Agency Company' }
+ assert_equal 'Agency Company', firm.name
+ end
+
+ def test_should_set_foreign_key_on_create_association
+ client = Client.create! :name => "fuu"
+
+ firm = client.create_firm :name => "baa"
+ assert_equal firm.id, client.client_of
+ end
+
+ def test_should_set_foreign_key_on_create_association!
+ client = Client.create! :name => "fuu"
+
+ firm = client.create_firm! :name => "baa"
+ assert_equal firm.id, client.client_of
+ end
+
+ def test_self_referential_belongs_to_with_counter_cache_assigning_nil
+ comment = Comment.create! :post => posts(:thinking), :body => "fuu"
+ comment.parent = nil
+ comment.save!
+
+ assert_equal nil, comment.reload.parent
+ assert_equal 0, comments(:greetings).reload.children_count
+ end
+
+ def test_polymorphic_with_custom_primary_key
+ toy = Toy.create!
+ sponsor = Sponsor.create!(:sponsorable => toy)
+
+ assert_equal toy, sponsor.reload.sponsorable
+ end
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 39e8a7960a..ff376a68d8 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -8,10 +8,12 @@ require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/person'
+require 'models/vertex'
+require 'models/edge'
class CascadedEagerLoadingTest < ActiveRecord::TestCase
fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments,
- :categorizations, :people, :categories
+ :categorizations, :people, :categories, :edges, :vertices
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
@@ -51,7 +53,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
assert_nothing_raised do
- assert_equal 3, categories.count
+ assert_equal 4, categories.count
+ assert_equal 4, categories.all.count
+ assert_equal 3, categories.count(:distinct => true)
assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
end
@@ -162,12 +166,6 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
authors[2].post_about_thinking.comments.first
end
end
-end
-
-require 'models/vertex'
-require 'models/edge'
-class CascadedEagerLoadingTest < ActiveRecord::TestCase
- fixtures :edges, :vertices
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id')
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 3e92a77830..c6e451fc57 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -380,6 +380,18 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id)
end
+ def test_string_id_column_joins
+ s = Subscriber.create! do |c|
+ c.id = "PL"
+ end
+
+ b = Book.create!
+
+ Subscription.create!(:subscriber_id => "PL", :book_id => b.id)
+ s.reload
+ s.book_ids = s.book_ids
+ end
+
def test_eager_load_has_many_through_with_string_keys
books = books(:awdr, :rfr)
subscriber = Subscriber.find(subscribers(:second).id, :include => :books)
@@ -448,6 +460,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal post_tags, eager_post_tags
end
+ def test_eager_with_has_many_through_join_model_ignores_default_includes
+ assert_nothing_raised do
+ authors(:david).comments_on_posts_with_default_include.to_a
+ end
+ end
+
def test_eager_with_has_many_and_limit
posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
assert_equal 2, posts.size
@@ -675,6 +693,46 @@ class EagerAssociationTest < ActiveRecord::TestCase
}
end
+ def test_eager_with_default_scope
+ developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first
+ projects = Project.order(:id).all
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_class_method
+ developer = EagerDeveloperWithClassMethodDefaultScope.where(:name => 'David').first
+ projects = Project.order(:id).all
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_lambda
+ developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first
+ projects = Project.order(:id).all
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_block
+ developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first
+ projects = Project.order(:id).all
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_callable
+ developer = EagerDeveloperWithCallableDefaultScope.where(:name => 'David').first
+ projects = Project.order(:id).all
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
def find_all_ordered(className, include=nil)
className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include)
end
@@ -982,4 +1040,24 @@ class EagerAssociationTest < ActiveRecord::TestCase
}
assert_no_queries { assert_equal groucho, sponsor.thing }
end
+
+ def test_joins_with_includes_should_preload_via_joins
+ post = assert_queries(1) { Post.includes(:comments).joins(:comments).order('posts.id desc').to_a.first }
+
+ assert_queries(0) do
+ assert_not_equal 0, post.comments.to_a.count
+ end
+ end
+
+ def test_join_eager_with_empty_order_should_generate_valid_sql
+ assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first
+ end
+ end
+
+ def test_join_eager_with_nil_order_should_generate_valid_sql
+ assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index 24830a661a..8dc1423375 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -36,18 +36,32 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
end
def test_marshalling_extensions
+ if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7"
+ return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \
+ "to be a Ruby bug.")
+ end
+
david = developers(:david)
assert_equal projects(:action_controller), david.projects.find_most_recent
- david = Marshal.load(Marshal.dump(david))
+ marshalled = Marshal.dump(david)
+ david = Marshal.load(marshalled)
+
assert_equal projects(:action_controller), david.projects.find_most_recent
end
def test_marshalling_named_extensions
+ if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7"
+ return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \
+ "to be a Ruby bug.")
+ end
+
david = developers(:david)
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
- david = Marshal.load(Marshal.dump(david))
+ marshalled = Marshal.dump(david)
+ david = Marshal.load(marshalled)
+
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
end
diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb
index 745f169ad7..fe2b82f2c1 100644
--- a/activerecord/test/cases/associations/habtm_join_table_test.rb
+++ b/activerecord/test/cases/associations/habtm_join_table_test.rb
@@ -32,13 +32,4 @@ class HabtmJoinTableTest < ActiveRecord::TestCase
ActiveRecord::Base.connection.drop_table :my_readers
ActiveRecord::Base.connection.drop_table :my_books_my_readers
end
-
- uses_transaction :test_should_raise_exception_when_join_table_has_a_primary_key
- def test_should_raise_exception_when_join_table_has_a_primary_key
- if ActiveRecord::Base.connection.supports_primary_key?
- assert_raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError do
- MyReader.has_and_belongs_to_many :my_books
- end
- end
- end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index f4d14853d3..34d90cc395 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -101,6 +101,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 't1', record[1]
end
+ def test_proper_usage_of_primary_keys_and_join_table
+ setup_data_for_habtm_case
+
+ assert_equal 'country_id', Country.primary_key
+ assert_equal 'treaty_id', Treaty.primary_key
+
+ country = Country.first
+ assert_equal 1, country.treaties.count
+ end
+
def test_has_and_belongs_to_many
david = Developer.find(1)
@@ -235,6 +245,21 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
+ def test_new_aliased_to_build
+ devel = Developer.find(1)
+ proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
+ assert !devel.projects.loaded?
+
+ assert_equal devel.projects.last, proj
+ assert devel.projects.loaded?
+
+ assert !proj.persisted?
+ devel.save
+ assert proj.persisted?
+ assert_equal devel.projects.last, proj
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
def test_build_by_new_record
devel = Developer.new(:name => "Marcel", :salary => 75000)
devel.projects.build(:name => "Make bed")
@@ -625,6 +650,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker
end
+ def test_habtm_selects_all_columns_by_default
+ assert_equal Project.column_names.sort, developers(:david).projects.first.attributes.keys.sort
+ end
+
+ def test_habtm_respects_select_query_method
+ assert_equal ['id'], developers(:david).projects.select(:id).first.attributes.keys
+ end
+
def test_join_table_alias
assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index b149f5912f..cddd2a6f8c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
+require 'models/contract'
require 'models/topic'
require 'models/reply'
require 'models/category'
@@ -11,10 +12,12 @@ require 'models/comment'
require 'models/person'
require 'models/reader'
require 'models/tagging'
+require 'models/tag'
require 'models/invoice'
require 'models/line_item'
require 'models/car'
require 'models/bulb'
+require 'models/engine'
class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase
class Invoice < ActiveRecord::Base
@@ -224,6 +227,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, Firm.find(:first, :order => "id").clients.length
end
+ def test_finding_array_compatibility
+ assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length
+ end
+
def test_find_with_blank_conditions
[[], {}, nil, ""].each do |blank|
assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).size
@@ -478,6 +485,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, authors(:mary).popular_grouped_posts.length
end
+ def test_default_select
+ assert_equal Comment.column_names.sort, posts(:welcome).comments.first.attributes.keys.sort
+ end
+
+ def test_select_query_method
+ assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys
+ end
+
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
@@ -535,6 +550,35 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
end
+ def test_transactions_when_adding_to_persisted
+ good = Client.new(:name => "Good")
+ bad = Client.new(:name => "Bad", :raise_on_save => true)
+
+ begin
+ companies(:first_firm).clients_of_firm.concat(good, bad)
+ rescue Client::RaisedOnSave
+ end
+
+ assert !companies(:first_firm).clients_of_firm(true).include?(good)
+ end
+
+ def test_transactions_when_adding_to_new_record
+ assert_no_queries do
+ firm = Firm.new
+ firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
+ end
+ end
+
+ def test_new_aliased_to_build
+ company = companies(:first_firm)
+ new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
+ assert !company.clients_of_firm.loaded?
+
+ assert_equal "Another Client", new_client.name
+ assert !new_client.persisted?
+ assert_equal new_client, company.clients_of_firm.last
+ end
+
def test_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
@@ -766,6 +810,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
+ def test_transaction_when_deleting_persisted
+ good = Client.new(:name => "Good")
+ bad = Client.new(:name => "Bad", :raise_on_destroy => true)
+
+ companies(:first_firm).clients_of_firm = [good, bad]
+
+ begin
+ companies(:first_firm).clients_of_firm.destroy(good, bad)
+ rescue Client::RaisedOnDestroy
+ end
+
+ assert_equal [good, bad], companies(:first_firm).clients_of_firm(true)
+ end
+
+ def test_transaction_when_deleting_new_record
+ assert_no_queries do
+ firm = Firm.new
+ client = Client.new("name" => "New Client")
+ firm.clients_of_firm << client
+ firm.clients_of_firm.destroy(client)
+ end
+ end
+
def test_clearing_an_association_collection
firm = companies(:first_firm)
client_id = firm.clients_of_firm.first.id
@@ -792,6 +859,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_clearing_updates_counter_cache_when_inverse_counter_cache_is_a_symbol_with_dependent_destroy
+ car = Car.first
+ car.engines.create!
+
+ assert_difference 'car.reload.engines_count', -1 do
+ car.engines.clear
+ end
+ end
+
def test_clearing_a_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
@@ -1099,6 +1175,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal orig_accounts, firm.accounts
end
+ def test_transactions_when_replacing_on_persisted
+ good = Client.new(:name => "Good")
+ bad = Client.new(:name => "Bad", :raise_on_save => true)
+
+ companies(:first_firm).clients_of_firm = [good]
+
+ begin
+ companies(:first_firm).clients_of_firm = [bad]
+ rescue Client::RaisedOnSave
+ end
+
+ assert_equal [good], companies(:first_firm).clients_of_firm(true)
+ end
+
+ def test_transactions_when_replacing_on_new_record
+ assert_no_queries do
+ firm = Firm.new
+ firm.clients_of_firm = [Client.new("name" => "New Client")]
+ end
+ end
+
def test_get_ids
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
end
@@ -1445,4 +1542,59 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_equal target.object_id, ary.object_id
end
+
+ def test_merging_with_custom_attribute_writer
+ bulb = Bulb.new(:color => "red")
+ assert_equal "RED!", bulb.color
+
+ car = Car.create!
+ car.bulbs << bulb
+
+ assert_equal "RED!", car.bulbs.to_a.first.color
+ end
+
+ def test_new_is_called_with_attributes_and_options
+ car = Car.create(:name => 'honda')
+
+ bulb = car.bulbs.build
+ assert_equal Bulb, bulb.class
+
+ bulb = car.bulbs.build(:bulb_type => :custom)
+ assert_equal Bulb, bulb.class
+
+ bulb = car.bulbs.build({ :bulb_type => :custom }, :as => :admin)
+ assert_equal CustomBulb, bulb.class
+ end
+
+ def test_abstract_class_with_polymorphic_has_many
+ post = SubStiPost.create! :title => "fooo", :body => "baa"
+ tagging = Tagging.create! :taggable => post
+ assert_equal [tagging], post.taggings
+ end
+
+ def test_dont_call_save_callbacks_twice_on_has_many
+ firm = companies(:first_firm)
+ contract = firm.contracts.create!
+
+ assert_equal 1, contract.hi_count
+ assert_equal 1, contract.bye_count
+ end
+
+ def test_association_attributes_are_available_to_after_initialize
+ car = Car.create(:name => 'honda')
+ bulb = car.bulbs.build
+
+ assert_equal car.id, bulb.attributes_after_initialize['car_id']
+ end
+
+ def test_replace
+ car = Car.create(:name => 'honda')
+ bulb1 = car.bulbs.create
+ bulb2 = Bulb.create
+
+ assert_equal [bulb1], car.bulbs
+ car.bulbs.replace([bulb2])
+ assert_equal [bulb2], car.bulbs
+ assert_equal [bulb2], car.reload.bulbs
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 89117593fd..b703c96ec1 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -714,6 +714,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [categories(:general).id], authors(:mary).categories_like_general_ids
end
+ def test_get_collection_singular_ids_on_has_many_through_with_conditions_and_include
+ person = Person.first
+ assert_equal person.posts_with_no_comment_ids, person.posts_with_no_comments.map(&:id)
+ end
+
def test_count_has_many_through_with_named_scope
assert_equal 2, authors(:mary).categories.count
assert_equal 1, authors(:mary).categories.general.count
@@ -766,4 +771,58 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [category.name], post.named_category_ids # checks when target loaded
assert_equal [category.name], post.reload.named_category_ids # checks when target no loaded
end
+
+ def test_create_should_not_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ Category.create(:name => 'Fishing', :authors => [Author.first])
+ end
+ end
+
+ def test_save_should_not_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ c = Category.create(:name => 'Fishing', :authors => [Author.first])
+ c.save
+ end
+ end
+
+ def test_create_bang_should_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Category.create!(:name => 'Fishing', :authors => [Author.first])
+ end
+ end
+ end
+
+ def test_save_bang_should_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ c = Category.new(:name => 'Fishing', :authors => [Author.first])
+ assert_raises(ActiveRecord::RecordInvalid) do
+ c.save!
+ end
+ end
+ end
+
+ def test_create_bang_returns_falsy_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ c = Category.new(:name => 'Fishing', :authors => [Author.first])
+ assert !c.save
+ end
+ end
+
+ def test_preloading_empty_through_association_via_joins
+ person = Person.create!(:first_name => "Gaga")
+ person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').includes(:posts).to_a.first
+
+ assert person.posts.loaded?, 'person.posts should be loaded'
+ assert_equal [], person.posts
+ end
+
+ def test_explicitly_joining_join_table
+ assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index f3c96ccbe6..26931e3e85 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -4,6 +4,7 @@ require 'models/project'
require 'models/company'
require 'models/ship'
require 'models/pirate'
+require 'models/car'
require 'models/bulb'
class HasOneAssociationsTest < ActiveRecord::TestCase
@@ -95,6 +96,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_natural_assignment_to_nil_after_destroy
+ firm = companies(:rails_core)
+ old_account_id = firm.account.id
+ firm.account.destroy
+ firm.account = nil
+ assert_nil companies(:rails_core).account
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
def test_association_change_calls_delete
companies(:first_firm).deletable_account = Account.new(:credit_limit => 5)
assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
@@ -335,6 +345,17 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert orig_ship.destroyed?
end
+ def test_creation_failure_due_to_new_record_should_raise_error
+ pirate = pirates(:redbeard)
+ new_ship = Ship.new
+
+ assert_raise(ActiveRecord::RecordNotSaved) do
+ pirate.ship = new_ship
+ end
+ assert_nil pirate.ship
+ assert_nil new_ship.pirate_id
+ end
+
def test_replacement_failure_due_to_existing_record_should_raise_error
pirate = pirates(:blackbeard)
pirate.ship.name = nil
@@ -359,4 +380,80 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal pirate.id, ships(:black_pearl).reload.pirate_id
assert_nil new_ship.pirate_id
end
+
+ def test_association_keys_bypass_attribute_protection
+ car = Car.create(:name => 'honda')
+
+ bulb = car.build_bulb
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.build_bulb :car_id => car.id + 1
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.create_bulb
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.create_bulb :car_id => car.id + 1
+ assert_equal car.id, bulb.car_id
+ end
+
+ def test_association_conditions_bypass_attribute_protection
+ car = Car.create(:name => 'honda')
+
+ bulb = car.build_frickinawesome_bulb
+ assert_equal true, bulb.frickinawesome?
+
+ bulb = car.build_frickinawesome_bulb(:frickinawesome => false)
+ assert_equal true, bulb.frickinawesome?
+
+ bulb = car.create_frickinawesome_bulb
+ assert_equal true, bulb.frickinawesome?
+
+ bulb = car.create_frickinawesome_bulb(:frickinawesome => false)
+ assert_equal true, bulb.frickinawesome?
+ end
+
+ def test_new_is_called_with_attributes_and_options
+ car = Car.create(:name => 'honda')
+
+ bulb = car.build_bulb
+ assert_equal Bulb, bulb.class
+
+ bulb = car.build_bulb
+ assert_equal Bulb, bulb.class
+
+ bulb = car.build_bulb(:bulb_type => :custom)
+ assert_equal Bulb, bulb.class
+
+ bulb = car.build_bulb({ :bulb_type => :custom }, :as => :admin)
+ assert_equal CustomBulb, bulb.class
+ end
+
+ def test_build_with_block
+ car = Car.create(:name => 'honda')
+
+ bulb = car.build_bulb{ |b| b.color = 'Red' }
+ assert_equal 'RED!', bulb.color
+ end
+
+ def test_create_with_block
+ car = Car.create(:name => 'honda')
+
+ bulb = car.create_bulb{ |b| b.color = 'Red' }
+ assert_equal 'RED!', bulb.color
+ end
+
+ def test_create_bang_with_block
+ car = Car.create(:name => 'honda')
+
+ bulb = car.create_bulb!{ |b| b.color = 'Red' }
+ assert_equal 'RED!', bulb.color
+ end
+
+ def test_association_attributes_are_available_to_after_initialize
+ car = Car.create(:name => 'honda')
+ bulb = car.create_bulb
+
+ assert_equal car.id, bulb.attributes_after_initialize['car_id']
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 968025ade8..2503349c08 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -310,4 +310,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal dashboard, minivan.dashboard
assert_equal dashboard, minivan.speedometer.dashboard
end
+
+ def test_has_one_through_with_custom_select_on_join_model_default_scope
+ assert_equal clubs(:boring_club), members(:groucho).selected_club
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 124693f7c9..e5e9ca6131 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/post'
require 'models/comment'
require 'models/author'
+require 'models/essay'
require 'models/category'
require 'models/categorization'
require 'models/person'
@@ -9,7 +10,7 @@ require 'models/tagging'
require 'models/tag'
class InnerJoinAssociationTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations,
+ fixtures :authors, :essays, :posts, :comments, :categories, :categories_posts, :categorizations,
:taggings, :tags
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 49a1c117bc..4ce8b85098 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -13,6 +13,9 @@ require 'models/vertex'
require 'models/edge'
require 'models/book'
require 'models/citation'
+require 'models/aircraft'
+require 'models/engine'
+require 'models/car'
class AssociationsJoinModelTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
@@ -136,7 +139,21 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_set_polymorphic_has_one
tagging = tags(:misc).taggings.create
posts(:thinking).tagging = tagging
- assert_equal "Post", tagging.taggable_type
+
+ assert_equal "Post", tagging.taggable_type
+ assert_equal posts(:thinking).id, tagging.taggable_id
+ assert_equal posts(:thinking), tagging.taggable
+ end
+
+ def test_set_polymorphic_has_one_on_new_record
+ tagging = tags(:misc).taggings.create
+ post = Post.new :title => "foo", :body => "bar"
+ post.tagging = tagging
+ post.save!
+
+ assert_equal "Post", tagging.taggable_type
+ assert_equal post.id, tagging.taggable_id
+ assert_equal post, tagging.taggable
end
def test_create_polymorphic_has_many_with_scope
@@ -704,6 +721,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
end
+ def test_has_many_with_pluralize_table_names_false
+ aircraft = Aircraft.create!(:name => "Airbus 380")
+ engine = Engine.create!(:car_id => aircraft.id)
+ assert_equal aircraft.engines, [engine]
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index dd450a2a8e..530f5212a2 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -247,7 +247,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'),
+ Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'),
[categories(:general), categories(:technology)], :post_comments
)
end
@@ -356,6 +356,17 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal categories(:general), members(:groucho).club_category
end
+ def test_joins_and_includes_from_through_models_not_included_in_association
+ prev_default_scope = Club.default_scopes
+
+ [:includes, :preload, :joins, :eager_load].each do |q|
+ Club.default_scopes = [Club.send(q, :category)]
+ assert_equal categories(:general), members(:groucho).reload.club_category
+ end
+ ensure
+ Club.default_scopes = prev_default_scope
+ end
+
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload
members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) }
general = categories(:general)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 49d82ba2df..ffe2993e0f 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -203,6 +203,11 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal david.projects, david.projects.reload.reload
end
end
+
+ def test_proxy_association_accessor
+ david = developers(:david)
+ assert_equal david.association(:projects), david.projects.proxy_association
+ end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 3641031d12..e03ed33591 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -35,6 +35,7 @@ module ActiveRecord
end
def self.serialized_attributes; {}; end
+ def self.base_class; self; end
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 5074ae50ab..b1b41fed0d 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -109,6 +109,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title
end
+ # IRB inspects the return value of "MyModel.allocate"
+ # by inspecting it.
+ def test_allocated_object_can_be_inspected
+ topic = Topic.allocate
+ topic.instance_eval { @attributes = nil }
+ assert_nothing_raised { topic.inspect }
+ assert topic.inspect, "#<Topic not initialized>"
+ end
+
def test_array_content
topic = Topic.new
topic.content = %w( one two three )
@@ -126,7 +135,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
if current_adapter?(:MysqlAdapter)
def test_read_attributes_before_type_cast_on_boolean
bool = Boolean.create({ "value" => false })
- assert_equal 0, bool.reload.attributes_before_type_cast["value"]
+ if RUBY_PLATFORM =~ /java/
+ # JRuby will return the value before typecast as string
+ assert_equal "0", bool.reload.attributes_before_type_cast["value"]
+ else
+ assert_equal 0, bool.reload.attributes_before_type_cast["value"]
+ end
end
end
@@ -417,30 +431,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert topic.is_test?
end
- def test_kernel_methods_not_implemented_in_activerecord
- %w(test name display y).each do |method|
- assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
- end
- end
-
- def test_defined_kernel_methods_implemented_in_model
- %w(test name display y).each do |method|
- klass = Class.new ActiveRecord::Base
- klass.class_eval "def #{method}() 'defined #{method}' end"
- assert klass.instance_method_already_implemented?(method), "##{method} is not defined"
- end
- end
-
- def test_defined_kernel_methods_implemented_in_model_abstract_subclass
- %w(test name display y).each do |method|
- abstract = Class.new ActiveRecord::Base
- abstract.class_eval "def #{method}() 'defined #{method}' end"
- abstract.abstract_class = true
- klass = Class.new abstract
- assert klass.instance_method_already_implemented?(method), "##{method} is not defined"
- end
- end
-
def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model
%w(save create_or_update).each do |method|
klass = Class.new ActiveRecord::Base
@@ -594,7 +584,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(:title => "The pros and cons of programming naked.")
assert !topic.respond_to?(:title)
exception = assert_raise(NoMethodError) { topic.title }
- assert_match %r(^Attempt to call private method), exception.message
+ assert exception.message.include?("private method")
assert_equal "I'm private", topic.send(:title)
end
@@ -604,7 +594,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new
assert !topic.respond_to?(:title=)
exception = assert_raise(NoMethodError) { topic.title = "Pants"}
- assert_match %r(^Attempt to call private method), exception.message
+ assert exception.message.include?("private method")
topic.send(:title=, "Very large pants")
end
@@ -614,7 +604,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(:title => "Isaac Newton's pants")
assert !topic.respond_to?(:title?)
exception = assert_raise(NoMethodError) { topic.title? }
- assert_match %r(^Attempt to call private method), exception.message
+ assert exception.message.include?("private method")
assert topic.send(:title?)
end
@@ -645,6 +635,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal %w(preferences), Contact.serialized_attributes.keys
end
+ def test_instance_method_should_be_defined_on_the_base_class
+ subklass = Class.new(Topic)
+
+ Topic.define_attribute_methods
+
+ instance = subklass.new
+ instance.id = 5
+ assert_equal 5, instance.id
+ assert subklass.method_defined?(:id), "subklass is missing id method"
+
+ Topic.undefine_attribute_methods
+
+ assert_equal 5, instance.id
+ assert subklass.method_defined?(:id), "subklass is missing id method"
+ end
+
+ def test_dispatching_column_attributes_through_method_missing_deprecated
+ Topic.define_attribute_methods
+
+ topic = Topic.new(:id => 5)
+ topic.id = 5
+
+ topic.method(:id).owner.send(:remove_method, :id)
+
+ assert_deprecated do
+ assert_equal 5, topic.id
+ end
+ ensure
+ Topic.undefine_attribute_methods
+ end
+
private
def cached_columns
@cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name)
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 8f55b7ebe6..4ad2cdfc7e 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -837,7 +837,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.mark_for_destruction }
assert @pirate.save
- assert_no_queries do
+ assert_queries(0) do
assert @pirate.save
end
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index fb4eacb632..12c1cfb30e 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -21,8 +21,11 @@ require 'models/parrot'
require 'models/person'
require 'models/edge'
require 'models/joke'
+require 'models/bulb'
+require 'models/bird'
require 'rexml/document'
require 'active_support/core_ext/exception'
+require 'bcrypt'
class Category < ActiveRecord::Base; end
class Categorization < ActiveRecord::Base; end
@@ -45,13 +48,50 @@ class ReadonlyTitlePost < Post
attr_readonly :title
end
+class ProtectedTitlePost < Post
+ attr_protected :title
+end
+
class Weird < ActiveRecord::Base; end
class Boolean < ActiveRecord::Base; end
+class LintTest < ActiveRecord::TestCase
+ include ActiveModel::Lint::Tests
+
+ class LintModel < ActiveRecord::Base; end
+
+ def setup
+ @model = LintModel.new
+ end
+end
+
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
+ def test_column_names_are_escaped
+ conn = ActiveRecord::Base.connection
+ classname = conn.class.name[/[^:]*$/]
+ badchar = {
+ 'SQLite3Adapter' => '"',
+ 'MysqlAdapter' => '`',
+ 'Mysql2Adapter' => '`',
+ 'PostgreSQLAdapter' => '"',
+ 'OracleAdapter' => '"',
+ }.fetch(classname) {
+ raise "need a bad char for #{classname}"
+ }
+
+ quoted = conn.quote_column_name "foo#{badchar}bar"
+ if current_adapter?(:OracleAdapter)
+ # Oracle does not allow double quotes in table and column names at all
+ # therefore quoting removes them
+ assert_equal("#{badchar}foobar#{badchar}", quoted)
+ else
+ assert_equal("#{badchar}foo#{badchar * 2}bar#{badchar}", quoted)
+ end
+ end
+
def test_columns_should_obey_set_primary_key
pk = Subscriber.columns.find { |x| x.name == 'nick' }
assert pk.primary, 'nick should be primary key'
@@ -129,25 +169,6 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_use_table_engine_for_quoting_where
- relation = Topic.where(Topic.arel_table[:id].eq(1))
- engine = relation.table.engine
-
- fakepool = Class.new(Struct.new(:spec)) {
- def with_connection; yield self; end
- def connection_pool; self; end
- def table_exists?(name); false; end
- def quote_table_name(*args); raise "lol quote_table_name"; end
- }
-
- relation.table.engine = fakepool.new(engine.connection_pool.spec)
-
- error = assert_raises(RuntimeError) { relation.to_a }
- assert_match('lol', error.message)
- ensure
- relation.table.engine = engine
- end
-
def test_preserving_time_objects
assert_kind_of(
Time, Topic.find(1).bonus_time,
@@ -246,6 +267,41 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def test_create_after_initialize_without_block
+ cb = CustomBulb.create(:name => 'Dude')
+ assert_equal('Dude', cb.name)
+ assert_equal(true, cb.frickinawesome)
+ end
+
+ def test_create_after_initialize_with_block
+ cb = CustomBulb.create {|c| c.name = 'Dude' }
+ assert_equal('Dude', cb.name)
+ assert_equal(true, cb.frickinawesome)
+ end
+
+ def test_first_or_create
+ parrot = Bird.first_or_create(:color => 'green', :name => 'parrot')
+ assert parrot.persisted?
+ the_same_parrot = Bird.first_or_create(:color => 'yellow', :name => 'macaw')
+ assert_equal parrot, the_same_parrot
+ end
+
+ def test_first_or_create_bang
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.first_or_create! }
+ parrot = Bird.first_or_create!(:color => 'green', :name => 'parrot')
+ assert parrot.persisted?
+ the_same_parrot = Bird.first_or_create!(:color => 'yellow', :name => 'macaw')
+ assert_equal parrot, the_same_parrot
+ end
+
+ def test_first_or_initialize
+ parrot = Bird.first_or_initialize(:color => 'green', :name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.new_record?
+ assert parrot.valid?
+ end
+
def test_load
topics = Topic.find(:all, :order => 'id')
assert_equal(4, topics.size)
@@ -363,6 +419,15 @@ class BasicsTest < ActiveRecord::TestCase
GUESSED_CLASSES.each(&:reset_table_name)
end
+ def test_singular_table_name_guesses_for_individual_table
+ CreditCard.pluralize_table_names = false
+ CreditCard.reset_table_name
+ assert_equal "credit_card", CreditCard.table_name
+ assert_equal "categories", Category.table_name
+ ensure
+ CreditCard.pluralize_table_names = true
+ CreditCard.reset_table_name
+ end
if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
def test_update_all_with_order_and_limit
@@ -466,6 +531,19 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
+ def test_comparison
+ topic_1 = Topic.create!
+ topic_2 = Topic.create!
+
+ assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
+ end
+
+ def test_comparison_with_different_objects
+ topic = Topic.create
+ category = Category.create(:name => "comparison")
+ assert_nil topic <=> category
+ end
+
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -489,12 +567,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 'value2', weird.send('a$b')
end
- def test_attributes_guard_protected_attributes_is_deprecated
- attributes = { "title" => "An amazing title" }
- topic = Topic.new
- assert_deprecated { topic.send(:attributes=, attributes, false) }
- end
-
def test_multiparameter_attributes_on_date
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
@@ -510,7 +582,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(1, 6, 24), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_empty_month
@@ -519,7 +591,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(2004, 1, 24), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_empty_day
@@ -528,7 +600,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(2004, 6, 1), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_empty_day_and_year
@@ -537,7 +609,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(1, 6, 1), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_empty_day_and_month
@@ -546,7 +618,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(2004, 1, 1), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_empty_year_and_month
@@ -555,7 +627,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(1, 1, 24), topic.last_read.to_date
+ assert_nil topic.last_read
end
def test_multiparameter_attributes_on_date_with_all_empty
@@ -648,12 +720,7 @@ class BasicsTest < ActiveRecord::TestCase
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal 1, topic.written_on.year
- assert_equal 1, topic.written_on.month
- assert_equal 1, topic.written_on.day
- assert_equal 0, topic.written_on.hour
- assert_equal 12, topic.written_on.min
- assert_equal 2, topic.written_on.sec
+ assert_nil topic.written_on
end
def test_multiparameter_attributes_on_time_will_ignore_date_if_empty
@@ -663,12 +730,7 @@ class BasicsTest < ActiveRecord::TestCase
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal 1, topic.written_on.year
- assert_equal 1, topic.written_on.month
- assert_equal 1, topic.written_on.day
- assert_equal 16, topic.written_on.hour
- assert_equal 24, topic.written_on.min
- assert_equal 0, topic.written_on.sec
+ assert_nil topic.written_on
end
def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty
attributes = {
@@ -677,12 +739,7 @@ class BasicsTest < ActiveRecord::TestCase
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal 1, topic.written_on.year
- assert_equal 1, topic.written_on.month
- assert_equal 1, topic.written_on.day
- assert_equal 16, topic.written_on.hour
- assert_equal 12, topic.written_on.min
- assert_equal 02, topic.written_on.sec
+ assert_nil topic.written_on
end
def test_multiparameter_attributes_on_time_with_utc
@@ -1083,6 +1140,17 @@ class BasicsTest < ActiveRecord::TestCase
self.table_name = 'numeric_data'
end
+ def test_big_decimal_conditions
+ m = NumericData.new(
+ :bank_balance => 1586.43,
+ :big_bank_balance => BigDecimal("1000234000567.95"),
+ :world_population => 6000000000,
+ :my_house_population => 3
+ )
+ assert m.save
+ assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count
+ end
+
def test_numeric_fields
m = NumericData.new(
:bank_balance => 1586.43,
@@ -1588,6 +1656,10 @@ class BasicsTest < ActiveRecord::TestCase
assert !LooseDescendant.abstract_class?
end
+ def test_abstract_class_table_name
+ assert_nil AbstractCompany.table_name
+ end
+
def test_base_class
assert_equal LoosePerson, LoosePerson.base_class
assert_equal LooseDescendant, LooseDescendant.base_class
@@ -1759,6 +1831,13 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def test_compute_type_argument_error
+ ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError)
+ assert_raises ArgumentError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ end
+
def test_clear_cache!
# preheat cache
c1 = Post.columns
@@ -1780,9 +1859,80 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_marshal_round_trip
+ if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7"
+ return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \
+ "to be a Ruby bug.")
+ end
+
expected = posts(:welcome)
- actual = Marshal.load(Marshal.dump(expected))
+ marshalled = Marshal.dump(expected)
+ actual = Marshal.load(marshalled)
assert_equal expected.attributes, actual.attributes
end
+
+ def test_marshal_new_record_round_trip
+ if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7"
+ return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \
+ "to be a Ruby bug.")
+ end
+
+ marshalled = Marshal.dump(Post.new)
+ post = Marshal.load(marshalled)
+
+ assert post.new_record?, "should be a new record"
+ end
+
+ def test_marshalling_with_associations
+ if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7"
+ return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \
+ "to be a Ruby bug.")
+ end
+
+ post = Post.new
+ post.comments.build
+
+ marshalled = Marshal.dump(post)
+ post = Marshal.load(marshalled)
+
+ assert_equal 1, post.comments.length
+ end
+
+ def test_attribute_names
+ assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"],
+ Company.attribute_names
+ end
+
+ def test_attribute_names_on_table_not_exists
+ assert_equal [], NonExistentTable.attribute_names
+ end
+
+ def test_attribtue_names_on_abstract_class
+ assert_equal [], AbstractCompany.attribute_names
+ end
+
+ def test_cache_key_for_existing_record_is_not_timezone_dependent
+ ActiveRecord::Base.time_zone_aware_attributes = true
+
+ Time.zone = "UTC"
+ utc_key = Developer.first.cache_key
+
+ Time.zone = "EST"
+ est_key = Developer.first.cache_key
+
+ assert_equal utc_key, est_key
+ ensure
+ ActiveRecord::Base.time_zone_aware_attributes = false
+ end
+
+ def test_cache_key_format_for_existing_record_with_updated_at
+ dev = Developer.first
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_nil_updated_at
+ dev = Developer.first
+ dev.update_attribute(:updated_at, nil)
+ assert_match(/\/#{dev.id}$/, dev.cache_key)
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 6620464d6a..660098b9ad 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -18,6 +18,13 @@ class EachTest < ActiveRecord::TestCase
end
end
+ def test_each_should_not_return_query_chain_and_execcute_only_one_query
+ assert_queries(1) do
+ result = Post.find_each(:batch_size => 100000){ }
+ assert_nil result
+ end
+ end
+
def test_each_should_raise_if_select_is_set_without_id
assert_raise(RuntimeError) do
Post.find_each(:select => :title, :batch_size => 1) { |post| post }
@@ -93,4 +100,40 @@ class EachTest < ActiveRecord::TestCase
end
end
end
+
+ def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
+ not_a_post = "not a post"
+ not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it")
+
+ assert_nothing_raised do
+ Post.find_in_batches(:batch_size => 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+
+ batch.map! { not_a_post }
+ end
+ end
+ end
+
+ def test_find_in_batches_should_ignore_the_order_default_scope
+ # First post is with title scope
+ first_post = PostWithDefaultScope.first
+ posts = []
+ PostWithDefaultScope.find_in_batches do |batch|
+ posts.concat(batch)
+ end
+ # posts.first will be ordered using id only. Title order scope should not apply here
+ assert_not_equal first_post, posts.first
+ assert_equal posts(:welcome), posts.first
+ end
+
+ def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
+ special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
+ posts = []
+ SpecialPostWithDefaultScope.find_in_batches do |batch|
+ posts.concat(batch)
+ end
+ assert_equal special_posts_ids, posts.map(&:id)
+ end
+
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 654c4c9010..c38814713a 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -170,6 +170,13 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_group_by_summed_field_having_condition_from_select
+ c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit)
+ assert_nil c[1]
+ assert_equal 60, c[2]
+ assert_equal 53, c[9]
+ end
+
def test_should_group_by_summed_association
c = Account.sum(:credit_limit, :group => :firm)
assert_equal 50, c[companies(:first_firm)]
@@ -319,6 +326,17 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
end
+ def test_should_not_perform_joined_include_by_default
+ assert_equal Account.count, Account.includes(:firm).count
+ queries = assert_sql { Account.includes(:firm).count }
+ assert_no_match(/join/i, queries.last)
+ end
+
+ def test_should_perform_joined_include_when_referencing_included_tables
+ joined_count = Account.includes(:firm).where(:companies => {:name => '37signals'}).count
+ assert_equal 1, joined_count
+ end
+
def test_should_count_scoped_select
Account.update_all("credit_limit = NULL")
assert_equal 0, Account.scoped(:select => "credit_limit").count
@@ -386,6 +404,10 @@ class CalculationsTest < ActiveRecord::TestCase
Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50")
end
+ def test_sum_array_compatibility
+ assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit)
+ end
+
def test_average_with_from_option
assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts')
assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"),
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index d1dddd4c2c..14884e42af 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -58,68 +58,68 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlColumn.new("title", "a", "binary(1)")
+ binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = MysqlColumn.new("title", "a", "varbinary(1)")
+ varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlColumn.new("title", "a", "blob")
+ MysqlAdapter::Column.new("title", "a", "blob")
end
assert_raise ArgumentError do
- MysqlColumn.new("title", "Hello", "text")
+ MysqlAdapter::Column.new("title", "Hello", "text")
end
- text_column = MysqlColumn.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, "text")
assert_equal nil, text_column.default
- not_null_text_column = MysqlColumn.new("title", nil, "text", false)
+ not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blog_and_test_data_types
- blob_column = MysqlColumn.new("title", nil, "blob")
+ blob_column = MysqlAdapter::Column.new("title", nil, "blob")
assert !blob_column.has_default?
- text_column = MysqlColumn.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, "text")
assert !text_column.has_default?
end
end
if current_adapter?(:Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Column.new("title", "a", "binary(1)")
+ binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = Mysql2Column.new("title", "a", "varbinary(1)")
+ varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- Mysql2Column.new("title", "a", "blob")
+ Mysql2Adapter::Column.new("title", "a", "blob")
end
assert_raise ArgumentError do
- Mysql2Column.new("title", "Hello", "text")
+ Mysql2Adapter::Column.new("title", "Hello", "text")
end
- text_column = Mysql2Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, "text")
assert_equal nil, text_column.default
- not_null_text_column = Mysql2Column.new("title", nil, "text", false)
+ not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blog_and_test_data_types
- blob_column = Mysql2Column.new("title", nil, "blob")
+ blob_column = Mysql2Adapter::Column.new("title", nil, "blob")
assert !blob_column.has_default?
- text_column = Mysql2Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, "text")
assert !text_column.has_default?
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index abf317768f..bd0d161838 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -6,7 +6,12 @@ module ActiveRecord
def setup
@handler = ConnectionHandler.new
@handler.establish_connection 'america', Base.connection_pool.spec
- @klass = Struct.new(:name).new('america')
+ @klass = Class.new do
+ def self.name; 'america'; end
+ end
+ @subklass = Class.new(@klass) do
+ def self.name; 'north america'; end
+ end
end
def test_retrieve_connection
@@ -28,6 +33,20 @@ module ActiveRecord
def test_retrieve_connection_pool
assert_not_nil @handler.retrieve_connection_pool(@klass)
end
+
+ def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection
+ assert_not_nil @handler.retrieve_connection_pool(@subklass)
+ end
+
+ def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove
+ @handler.establish_connection 'north america', Base.connection_pool.spec
+ assert_not_same @handler.retrieve_connection_pool(@klass),
+ @handler.retrieve_connection_pool(@subklass)
+
+ @handler.remove_connection @subklass
+ assert_same @handler.retrieve_connection_pool(@klass),
+ @handler.retrieve_connection_pool(@subklass)
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index 85871aebdf..f554ceef35 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -25,6 +25,40 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
+ class FakeBase < ActiveRecord::Base
+ def self.establish_connection spec
+ String === spec ? super : spec
+ end
+ end
+
+ def test_url_host_no_db
+ spec = FakeBase.establish_connection 'postgres://foo?encoding=utf8'
+ assert_equal({
+ :adapter => "postgresql",
+ :database => "",
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+
+ def test_url_host_db
+ spec = FakeBase.establish_connection 'postgres://foo/bar?encoding=utf8'
+ assert_equal({
+ :adapter => "postgresql",
+ :database => "bar",
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+
+ def test_url_port
+ spec = FakeBase.establish_connection 'postgres://foo:123?encoding=utf8'
+ assert_equal({
+ :adapter => "postgresql",
+ :database => "",
+ :port => 123,
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+
def test_app_delegation
manager = ConnectionManagement.new(@app)
@@ -77,6 +111,13 @@ module ActiveRecord
@management.call(@env)
assert ActiveRecord::Base.connection_handler.active_connections?
end
+
+ test "proxy is polite to it's body and responds to it" do
+ body = Class.new(String) { def to_path; "/path"; end }.new
+ proxy = ConnectionManagement::Proxy.new(body)
+ assert proxy.respond_to?(:to_path)
+ assert_equal proxy.to_path, "/path"
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index f92f4e62c5..8a0f453127 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -135,6 +135,10 @@ module ActiveRecord
pool.with_connection
end
end
+
+ def test_pool_sets_connection_visitor
+ assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
+ end
end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 4e75eafe3d..3088ab012f 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -48,6 +48,15 @@ class FinderTest < ActiveRecord::TestCase
assert Topic.exists?
end
+ # exists? should handle nil for id's that come from URLs and always return false
+ # (example: Topic.exists?(params[:id])) where params[:id] is nil
+ def test_exists_with_nil_arg
+ assert !Topic.exists?(nil)
+ assert Topic.exists?
+ assert !Topic.first.replies.exists?(nil)
+ assert Topic.first.replies.exists?
+ end
+
def test_does_not_exist_with_empty_table_and_no_args_given
Topic.delete_all
assert !Topic.exists?
@@ -140,23 +149,30 @@ class FinderTest < ActiveRecord::TestCase
def test_find_with_group
- developers = Developer.find(:all, :group => "salary", :select => "salary")
+ developers = Developer.find(:all, :group => "salary", :select => "salary")
assert_equal 4, developers.size
assert_equal 4, developers.map(&:salary).uniq.size
end
def test_find_with_group_and_having
- developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary")
+ developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary")
assert_equal 3, developers.size
assert_equal 3, developers.map(&:salary).uniq.size
- assert developers.all? { |developer| developer.salary > 10000 }
+ assert developers.all? { |developer| developer.salary > 10000 }
end
def test_find_with_group_and_sanitized_having
- developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary")
+ developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary")
assert_equal 3, developers.size
assert_equal 3, developers.map(&:salary).uniq.size
- assert developers.all? { |developer| developer.salary > 10000 }
+ assert developers.all? { |developer| developer.salary > 10000 }
+ end
+
+ def test_find_with_group_and_sanitized_having_method
+ developers = Developer.group(:salary).having("sum(salary) > ?", 10000).select('salary').all
+ assert_equal 3, developers.size
+ assert_equal 3, developers.map(&:salary).uniq.size
+ assert developers.all? { |developer| developer.salary > 10000 }
end
def test_find_with_entire_select_statement
@@ -236,6 +252,32 @@ class FinderTest < ActiveRecord::TestCase
end
end
+ def test_first_and_last_with_integer_should_use_sql_limit
+ assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries }
+ assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries }
+ end
+
+ def test_last_with_integer_and_order_should_keep_the_order
+ assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2)
+ end
+
+ def test_last_with_integer_and_order_should_not_use_sql_limit
+ query = assert_sql { Topic.order("title").last(5).entries }
+ assert_equal 1, query.length
+ assert_no_match(/LIMIT/, query.first)
+ end
+
+ def test_last_with_integer_and_reorder_should_not_use_sql_limit
+ query = assert_sql { Topic.reorder("title").last(5).entries }
+ assert_equal 1, query.length
+ assert_no_match(/LIMIT/, query.first)
+ end
+
+ def test_first_and_last_with_integer_should_return_an_array
+ assert_kind_of Array, Topic.first(5)
+ assert_kind_of Array, Topic.last(5)
+ end
+
def test_unexisting_record_exception_handling
assert_raise(ActiveRecord::RecordNotFound) {
Topic.find(1).parent
@@ -659,6 +701,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary")
end
+ def test_find_by_two_attributes_but_passing_only_one
+ assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
+ end
+
def test_find_last_by_one_attribute
assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title)
assert_nil Topic.find_last_by_title("A title with no matches")
@@ -940,6 +986,10 @@ class FinderTest < ActiveRecord::TestCase
assert !another.persisted?
end
+ def test_find_or_initialize_from_two_attributes_but_passing_only_one
+ assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") }
+ end
+
def test_find_or_initialize_from_one_aggregate_attribute_and_one_not
new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth")
assert_equal 123, new_customer.balance.amount
diff --git a/activerecord/test/cases/fixtures/file_test.rb b/activerecord/test/cases/fixtures/file_test.rb
new file mode 100644
index 0000000000..e623fbe4d1
--- /dev/null
+++ b/activerecord/test/cases/fixtures/file_test.rb
@@ -0,0 +1,83 @@
+require 'cases/helper'
+require 'tempfile'
+
+module ActiveRecord
+ class Fixtures
+ class FileTest < ActiveRecord::TestCase
+ def test_open
+ fh = File.open(::File.join(FIXTURES_ROOT, "accounts.yml"))
+ assert_equal 6, fh.to_a.length
+ end
+
+ def test_open_with_block
+ called = false
+ File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh|
+ called = true
+ assert_equal 6, fh.to_a.length
+ end
+ assert called, 'block called'
+ end
+
+ def test_names
+ File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh|
+ assert_equal ["signals37",
+ "unknown",
+ "rails_core_account",
+ "last_account",
+ "rails_core_account_2",
+ "odegy_account"].sort, fh.to_a.map(&:first).sort
+ end
+ end
+
+ def test_values
+ File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh|
+ assert_equal [1,2,3,4,5,6].sort, fh.to_a.map(&:last).map { |x|
+ x['id']
+ }.sort
+ end
+ end
+
+ def test_erb_processing
+ File.open(::File.join(FIXTURES_ROOT, "developers.yml")) do |fh|
+ devs = Array.new(8) { |i| "dev_#{i + 3}" }
+ assert_equal [], devs - fh.to_a.map(&:first)
+ end
+ end
+
+ def test_empty_file
+ tmp_yaml ['empty', 'yml'], '' do |t|
+ assert_equal [], File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+
+ # A valid YAML file is not necessarily a value Fixture file. Make sure
+ # an exception is raised if the format is not valid Fixture format.
+ def test_wrong_fixture_format_string
+ tmp_yaml ['empty', 'yml'], 'qwerty' do |t|
+ assert_raises(ActiveRecord::Fixture::FormatError) do
+ File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+ end
+
+ def test_wrong_fixture_format_nested
+ tmp_yaml ['empty', 'yml'], 'one: two' do |t|
+ assert_raises(ActiveRecord::Fixture::FormatError) do
+ File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+ end
+
+ private
+ def tmp_yaml(name, contents)
+ t = Tempfile.new name
+ t.binmode
+ t.write contents
+ t.close
+ yield t
+ ensure
+ t.close true
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0bd9c5763..1166c45843 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -20,6 +20,7 @@ require 'models/book'
require 'models/admin'
require 'models/admin/account'
require 'models/admin/user'
+require 'tempfile'
class FixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
@@ -45,6 +46,21 @@ class FixturesTest < ActiveRecord::TestCase
end
end
+ def test_broken_yaml_exception
+ badyaml = Tempfile.new ['foo', '.yml']
+ badyaml.write 'a: !ruby.yaml.org,2002:str |\nfoo'
+ badyaml.flush
+
+ dir = File.dirname badyaml.path
+ name =File.basename badyaml.path, '.yml'
+ assert_raises(ActiveRecord::Fixture::FormatError) do
+ ActiveRecord::Fixtures.create_fixtures(dir, name)
+ end
+ ensure
+ badyaml.close
+ badyaml.unlink
+ end
+
def test_create_fixtures
ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots")
assert Parrot.find_by_name('Curious George'), 'George is in the database'
@@ -174,21 +190,13 @@ class FixturesTest < ActiveRecord::TestCase
end
end
- def test_empty_csv_fixtures
- assert_deprecated do
- assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/csv/accounts")
- end
- end
-
def test_omap_fixtures
assert_nothing_raised do
fixtures = ActiveRecord::Fixtures.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered")
- i = 0
- fixtures.each do |name, fixture|
+ fixtures.each.with_index do |(name, fixture), i|
assert_equal "fixture_no_#{i}", name
assert_equal "Category #{i}", fixture['name']
- i += 1
end
end
end
@@ -443,14 +451,36 @@ end
class CustomConnectionFixturesTest < ActiveRecord::TestCase
set_fixture_class :courses => Course
fixtures :courses
- # Set to false to blow away fixtures cache and ensure our fixtures are loaded
- # and thus takes into account our set_fixture_class
self.use_transactional_fixtures = false
def test_connection
assert_kind_of Course, courses(:ruby)
assert_equal Course.connection, courses(:ruby).connection
end
+
+ def test_leaky_destroy
+ assert_nothing_raised { courses(:ruby) }
+ courses(:ruby).destroy
+ end
+
+ def test_it_twice_in_whatever_order_to_check_for_fixture_leakage
+ test_leaky_destroy
+ end
+end
+
+class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase
+ set_fixture_class :courses => Course
+ fixtures :courses
+ self.use_transactional_fixtures = true
+
+ def test_leaky_destroy
+ assert_nothing_raised { courses(:ruby) }
+ courses(:ruby).destroy
+ end
+
+ def test_it_twice_in_whatever_order_to_check_for_fixture_leakage
+ test_leaky_destroy
+ end
end
class InvalidTableNameFixturesTest < ActiveRecord::TestCase
@@ -488,7 +518,9 @@ class ManyToManyFixturesWithClassDefined < ActiveRecord::TestCase
end
class FixturesBrokenRollbackTest < ActiveRecord::TestCase
- def blank_setup; end
+ def blank_setup
+ @fixture_connections = [ActiveRecord::Base.connection]
+ end
alias_method :ar_setup_fixtures, :setup_fixtures
alias_method :setup_fixtures, :blank_setup
alias_method :setup, :blank_setup
diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb
index f2b91d977e..2ce0de360e 100644
--- a/activerecord/test/cases/habtm_destroy_order_test.rb
+++ b/activerecord/test/cases/habtm_destroy_order_test.rb
@@ -16,6 +16,16 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
assert !sicp.destroyed?
end
+ test 'should not raise error if have foreign key in the join table' do
+ student = Student.new(:name => "Ben Bitdiddle")
+ lesson = Lesson.new(:name => "SICP")
+ lesson.students << student
+ lesson.save!
+ assert_nothing_raised do
+ student.destroy
+ end
+ end
+
test "not destroying a student with lessons leaves student<=>lesson association intact" do
# test a normal before_destroy doesn't destroy the habtm joins
begin
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index fbb4ee6f7b..6735bc521b 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,8 +1,5 @@
require File.expand_path('../../../../load_paths', __FILE__)
-lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib")
-$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
-
require 'config'
require 'test/unit'
@@ -11,24 +8,24 @@ require 'mocha'
require 'active_record'
require 'active_support/dependencies'
-begin
- require 'connection'
-rescue LoadError
- # If we cannot load connection we assume that driver was not loaded for this test case, so we load sqlite3 as default one.
- # This allows for running separate test cases by simply running test file.
- connection_type = defined?(JRUBY_VERSION) ? 'jdbc' : 'native'
- require "test/connections/#{connection_type}_sqlite3/connection"
-end
+
+require 'support/config'
+require 'support/connection'
+
+# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Enable Identity Map only when ENV['IM'] is set to "true"
+ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "true")
+
+# Connect to the database
+ARTest.connect
+
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
-# Enable Identity Map for testing
-ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "false" ? false : true)
-
def current_adapter?(*types)
types.any? do |type|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
@@ -61,15 +58,15 @@ end
module ActiveRecord
class SQLCounter
- IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/]
+ cattr_accessor :ignored_sql
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
# FIXME: this needs to be refactored so specific database can add their own
# ignored SQL. This ignored SQL is for Oracle.
- IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- def initialize
- $queries_executed = []
- end
+ cattr_accessor :log
+ self.log = []
def call(name, start, finish, message_id, values)
sql = values[:sql]
@@ -77,10 +74,11 @@ module ActiveRecord
# FIXME: this seems bad. we should probably have a better way to indicate
# the query was cached
unless 'CACHE' == values[:name]
- $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
+ self.class.log << sql unless self.class.ignored_sql.any? { |r| sql =~ r }
end
end
end
+
ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
end
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
index 469f513e68..a428f1d87b 100644
--- a/activerecord/test/cases/i18n_test.rb
+++ b/activerecord/test/cases/i18n_test.rb
@@ -43,4 +43,3 @@ class ActiveRecordI18nTests < ActiveRecord::TestCase
assert_equal 'topic model', Reply.model_name.human
end
end
-
diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb
index a0e16400d2..3efc8bf559 100644
--- a/activerecord/test/cases/identity_map_test.rb
+++ b/activerecord/test/cases/identity_map_test.rb
@@ -140,7 +140,7 @@ class IdentityMapTest < ActiveRecord::TestCase
assert_not_same(p1, p2)
end
end
-
+
def test_inherited_with_type_attribute_without_identity_map
ActiveRecord::IdentityMap.without do
c = comments(:sub_special_comment)
@@ -164,7 +164,7 @@ class IdentityMapTest < ActiveRecord::TestCase
end
##############################################################################
- # Tests checking dirty attribute behaviour with IM #
+ # Tests checking dirty attribute behavior with IM #
##############################################################################
def test_loading_new_instance_should_not_update_dirty_attributes
@@ -238,7 +238,7 @@ class IdentityMapTest < ActiveRecord::TestCase
end
##############################################################################
- # Tests checking Identity Map behaviour with preloaded associations, joins, #
+ # Tests checking Identity Map behavior with preloaded associations, joins, #
# includes etc. #
##############################################################################
diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb
index 2de50b224c..98cda010ae 100644
--- a/activerecord/test/cases/invalid_date_test.rb
+++ b/activerecord/test/cases/invalid_date_test.rb
@@ -24,9 +24,9 @@ class InvalidDateTest < ActiveRecord::TestCase
topic = Topic.new({"last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s})
# Oracle DATE columns are datetime columns and Oracle adapter returns Time value
if current_adapter?(:OracleAdapter)
- assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object")
+ assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
else
- assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object")
+ assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
end
end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index afec64750e..3ae7b63dff 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -27,6 +27,19 @@ module ActiveRecord
end
end
+ class LegacyMigration < ActiveRecord::Migration
+ def self.up
+ create_table("horses") do |t|
+ t.column :content, :text
+ t.column :remind_at, :datetime
+ end
+ end
+
+ def self.down
+ drop_table("horses")
+ end
+ end
+
def teardown
if ActiveRecord::Base.connection.table_exists?("horses")
ActiveRecord::Base.connection.drop_table("horses")
@@ -41,17 +54,39 @@ module ActiveRecord
end
end
- def test_up
+ def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
assert migration.connection.table_exists?("horses"), "horses should exist"
end
- def test_down
+ def test_migrate_down
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
assert !migration.connection.table_exists?("horses")
end
+
+ def test_legacy_up
+ LegacyMigration.migrate :up
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
+ end
+
+ def test_legacy_down
+ LegacyMigration.migrate :up
+ LegacyMigration.migrate :down
+ assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ end
+
+ def test_up
+ LegacyMigration.up
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
+ end
+
+ def test_down
+ LegacyMigration.up
+ LegacyMigration.down
+ assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ end
end
end
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index 8664d63e8f..d9e350abc0 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -161,6 +161,15 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
assert_match %r{"tag":\{"name":"General"\}}, json
end
+ def test_includes_doesnt_merge_opts_from_base
+ json = @david.to_json(
+ :only => :id,
+ :include => :posts
+ )
+
+ assert_match %{"title":"Welcome to the weblog"}, json
+ end
+
def test_should_not_call_methods_on_associations_that_dont_respond
def @david.favorite_quote; "Constraints are liberating"; end
json = @david.to_json(:include => :posts, :methods => :favorite_quote)
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
index 643e949087..75e5dfa49b 100644
--- a/activerecord/test/cases/lifecycle_test.rb
+++ b/activerecord/test/cases/lifecycle_test.rb
@@ -231,6 +231,18 @@ class LifecycleTest < ActiveRecord::TestCase
assert_not_nil observer.topic_ids.last
end
+ test "able to disable observers" do
+ observer = DeveloperObserver.instance # activate
+ observer.calls.clear
+
+ ActiveRecord::Base.observers.disable DeveloperObserver do
+ Developer.create! :name => 'Ancestor', :salary => 100000
+ SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
+ end
+
+ assert_equal [], observer.calls
+ end
+
def test_observer_is_called_once
observer = DeveloperObserver.instance # activate
observer.calls.clear
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index c6c6079490..9e8475465e 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -63,6 +63,14 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_exists_query_logging
+ Developer.exists? 1
+ wait
+ assert_equal 1, @logger.logged(:debug).size
+ assert_match(/Developer Exists/, @logger.logged(:debug).last)
+ assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
+ end
+
def test_cached_queries
ActiveRecord::Base.cache do
Developer.all
diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb
index 062a642e50..ef35f3341e 100644
--- a/activerecord/test/cases/mass_assignment_security_test.rb
+++ b/activerecord/test/cases/mass_assignment_security_test.rb
@@ -181,6 +181,18 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase
assert_admin_attributes(p, true)
end
+ def test_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ p = TightPerson.create!(attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p, true)
+ end
+
+ def test_create_with_bang_with_admin_role_with_attr_protected_attributes
+ p = LoosePerson.create!(attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p, true)
+ end
+
def test_new_with_without_protection_with_attr_accessible_attributes
p = TightPerson.new(attributes_hash, :without_protection => true)
@@ -205,6 +217,18 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase
assert_all_attributes(p)
end
+ def test_create_with_bang_with_without_protection_with_attr_accessible_attributes
+ p = TightPerson.create!(attributes_hash, :without_protection => true)
+
+ assert_all_attributes(p)
+ end
+
+ def test_create_with_bang_with_without_protection_with_attr_protected_attributes
+ p = LoosePerson.create!(attributes_hash, :without_protection => true)
+
+ assert_all_attributes(p)
+ end
+
def test_protection_against_class_attribute_writers
[:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names,
:default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method|
@@ -215,6 +239,54 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase
end
end
+ def test_find_or_initialize_by_with_attr_accessible_attributes
+ p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash)
+
+ assert_default_attributes(p)
+ end
+
+ def test_find_or_initialize_by_with_admin_role_with_attr_accessible_attributes
+ p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p)
+ end
+
+ def test_find_or_initialize_by_with_attr_protected_attributes
+ p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash)
+
+ assert_default_attributes(p)
+ end
+
+ def test_find_or_initialize_by_with_admin_role_with_attr_protected_attributes
+ p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p)
+ end
+
+ def test_find_or_create_by_with_attr_accessible_attributes
+ p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash)
+
+ assert_default_attributes(p, true)
+ end
+
+ def test_find_or_create_by_with_admin_role_with_attr_accessible_attributes
+ p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p, true)
+ end
+
+ def test_find_or_create_by_with_attr_protected_attributes
+ p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash)
+
+ assert_default_attributes(p, true)
+ end
+
+ def test_find_or_create_by_with_admin_role_with_attr_protected_attributes
+ p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin)
+
+ assert_admin_attributes(p, true)
+ end
+
end
@@ -312,81 +384,81 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase
# build
- def test_has_one_build_with_attr_protected_attributes
+ def test_belongs_to_build_with_attr_protected_attributes
best_friend = @person.build_best_friend_of(attributes_hash)
assert_default_attributes(best_friend)
end
- def test_has_one_build_with_attr_accessible_attributes
+ def test_belongs_to_build_with_attr_accessible_attributes
best_friend = @person.build_best_friend_of(attributes_hash)
assert_default_attributes(best_friend)
end
- def test_has_one_build_with_admin_role_with_attr_protected_attributes
+ def test_belongs_to_build_with_admin_role_with_attr_protected_attributes
best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend)
end
- def test_has_one_build_with_admin_role_with_attr_accessible_attributes
+ def test_belongs_to_build_with_admin_role_with_attr_accessible_attributes
best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend)
end
- def test_has_one_build_without_protection
+ def test_belongs_to_build_without_protection
best_friend = @person.build_best_friend_of(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
# create
- def test_has_one_create_with_attr_protected_attributes
+ def test_belongs_to_create_with_attr_protected_attributes
best_friend = @person.create_best_friend_of(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_attr_accessible_attributes
+ def test_belongs_to_create_with_attr_accessible_attributes
best_friend = @person.create_best_friend_of(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_admin_role_with_attr_protected_attributes
+ def test_belongs_to_create_with_admin_role_with_attr_protected_attributes
best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_admin_role_with_attr_accessible_attributes
+ def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes
best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_without_protection
+ def test_belongs_to_create_without_protection
best_friend = @person.create_best_friend_of(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
# create!
- def test_has_one_create_with_bang_with_attr_protected_attributes
+ def test_belongs_to_create_with_bang_with_attr_protected_attributes
best_friend = @person.create_best_friend!(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_attr_accessible_attributes
+ def test_belongs_to_create_with_bang_with_attr_accessible_attributes
best_friend = @person.create_best_friend!(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes
+ def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes
best_friend = @person.create_best_friend!(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes
best_friend = @person.create_best_friend!(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_without_protection
+ def test_belongs_to_create_with_bang_without_protection
best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
@@ -400,83 +472,328 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase
# build
- def test_has_one_build_with_attr_protected_attributes
+ def test_has_many_build_with_attr_protected_attributes
best_friend = @person.best_friends.build(attributes_hash)
assert_default_attributes(best_friend)
end
- def test_has_one_build_with_attr_accessible_attributes
+ def test_has_many_build_with_attr_accessible_attributes
best_friend = @person.best_friends.build(attributes_hash)
assert_default_attributes(best_friend)
end
- def test_has_one_build_with_admin_role_with_attr_protected_attributes
+ def test_has_many_build_with_admin_role_with_attr_protected_attributes
best_friend = @person.best_friends.build(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend)
end
- def test_has_one_build_with_admin_role_with_attr_accessible_attributes
+ def test_has_many_build_with_admin_role_with_attr_accessible_attributes
best_friend = @person.best_friends.build(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend)
end
- def test_has_one_build_without_protection
+ def test_has_many_build_without_protection
best_friend = @person.best_friends.build(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
# create
- def test_has_one_create_with_attr_protected_attributes
+ def test_has_many_create_with_attr_protected_attributes
best_friend = @person.best_friends.create(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_attr_accessible_attributes
+ def test_has_many_create_with_attr_accessible_attributes
best_friend = @person.best_friends.create(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_admin_role_with_attr_protected_attributes
+ def test_has_many_create_with_admin_role_with_attr_protected_attributes
best_friend = @person.best_friends.create(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_admin_role_with_attr_accessible_attributes
+ def test_has_many_create_with_admin_role_with_attr_accessible_attributes
best_friend = @person.best_friends.create(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_without_protection
+ def test_has_many_create_without_protection
best_friend = @person.best_friends.create(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
# create!
- def test_has_one_create_with_bang_with_attr_protected_attributes
+ def test_has_many_create_with_bang_with_attr_protected_attributes
best_friend = @person.best_friends.create!(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_attr_accessible_attributes
+ def test_has_many_create_with_bang_with_attr_accessible_attributes
best_friend = @person.best_friends.create!(attributes_hash)
assert_default_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes
+ def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes
best_friend = @person.best_friends.create!(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes
best_friend = @person.best_friends.create!(attributes_hash, :as => :admin)
assert_admin_attributes(best_friend, true)
end
- def test_has_one_create_with_bang_without_protection
+ def test_has_many_create_with_bang_without_protection
best_friend = @person.best_friends.create!(attributes_hash, :without_protection => true)
assert_all_attributes(best_friend)
end
end
+
+
+class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase
+ include MassAssignmentTestHelpers
+
+ def nested_attributes_hash(association, collection = false, except = [:id])
+ if collection
+ { :first_name => 'David' }.merge(:"#{association}_attributes" => [attributes_hash.except(*except)])
+ else
+ { :first_name => 'David' }.merge(:"#{association}_attributes" => attributes_hash.except(*except))
+ end
+ end
+
+ # build
+
+ def test_has_one_new_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend)
+ end
+
+ def test_has_one_new_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend)
+ end
+
+ def test_has_one_new_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend)
+ end
+
+ def test_has_one_new_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend)
+ end
+
+ def test_has_one_new_without_protection
+ person = LoosePerson.new(nested_attributes_hash(:best_friend, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend)
+ end
+
+ def test_belongs_to_new_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of)
+ end
+
+ def test_belongs_to_new_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of)
+ end
+
+ def test_belongs_to_new_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of)
+ end
+
+ def test_belongs_to_new_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of)
+ end
+
+ def test_belongs_to_new_without_protection
+ person = LoosePerson.new(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend_of)
+ end
+
+ def test_has_many_new_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first)
+ end
+
+ def test_has_many_new_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first)
+ end
+
+ def test_has_many_new_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.new(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first)
+ end
+
+ def test_has_many_new_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.new(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first)
+ end
+
+ def test_has_many_new_without_protection
+ person = LoosePerson.new(nested_attributes_hash(:best_friends, true, nil), :without_protection => true)
+ assert_all_attributes(person.best_friends.first)
+ end
+
+ # create
+
+ def test_has_one_create_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_without_protection
+ person = LoosePerson.create(nested_attributes_hash(:best_friend, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend)
+ end
+
+ def test_belongs_to_create_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_without_protection
+ person = LoosePerson.create(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend_of)
+ end
+
+ def test_has_many_create_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_without_protection
+ person = LoosePerson.create(nested_attributes_hash(:best_friends, true, nil), :without_protection => true)
+ assert_all_attributes(person.best_friends.first)
+ end
+
+ # create!
+
+ def test_has_one_create_with_bang_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_bang_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friend))
+ assert_default_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friend), :as => :admin)
+ assert_admin_attributes(person.best_friend, true)
+ end
+
+ def test_has_one_create_with_bang_without_protection
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend)
+ end
+
+ def test_belongs_to_create_with_bang_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_bang_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friend_of))
+ assert_default_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin)
+ assert_admin_attributes(person.best_friend_of, true)
+ end
+
+ def test_belongs_to_create_with_bang_without_protection
+ person = LoosePerson.create!(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true)
+ assert_all_attributes(person.best_friend_of)
+ end
+
+ def test_has_many_create_with_bang_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_bang_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friends, true))
+ assert_default_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes
+ person = LoosePerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes
+ person = TightPerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin)
+ assert_admin_attributes(person.best_friends.first, true)
+ end
+
+ def test_has_many_create_with_bang_without_protection
+ person = LoosePerson.create!(nested_attributes_hash(:best_friends, true, nil), :without_protection => true)
+ assert_all_attributes(person.best_friends.first)
+ end
+
+end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index a0cb5dbdc5..0ab4f30363 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -14,7 +14,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_set_conditions
Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do
- assert_match '(just a test...)', Developer.scoped.arel.to_sql
+ assert_match '(just a test...)', Developer.scoped.to_sql
end
end
@@ -274,7 +274,7 @@ class NestedScopingTest < ActiveRecord::TestCase
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
Developer.send(:with_scope, :find => { :limit => 10 }) do
devs = Developer.scoped
- assert_match '(salary = 80000)', devs.arel.to_sql
+ assert_match '(salary = 80000)', devs.to_sql
assert_equal 10, devs.taken
end
end
@@ -308,7 +308,7 @@ class NestedScopingTest < ActiveRecord::TestCase
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
devs = Developer.scoped
- assert_match "(name = 'David') AND (salary = 80000)", devs.arel.to_sql
+ assert_match "(name = 'David') AND (salary = 80000)", devs.to_sql
assert_equal(1, Developer.count)
end
Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
@@ -321,7 +321,7 @@ class NestedScopingTest < ActiveRecord::TestCase
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
devs = Developer.scoped
- assert_match "(salary = 80000) AND (name = 'David')", devs.arel.to_sql
+ assert_match "(salary = 80000) AND (name = 'David')", devs.to_sql
assert_equal 10, devs.taken
end
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index ae531ebb4c..d108b456f0 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -29,7 +29,12 @@ module ActiveRecord
assert_equal [[:create_table, [:horses]]], recorder.commands
end
- def test_unknown_commands_raise_exception
+ def test_unknown_commands_delegate
+ recorder = CommandRecorder.new(stub(:foo => 'bar'))
+ assert_equal 'bar', recorder.foo
+ end
+
+ def test_unknown_commands_raise_exception_if_they_cannot_delegate
@recorder.record :execute, ['some sql']
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse
@@ -86,10 +91,22 @@ module ActiveRecord
assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
end
+ def test_invert_add_index_with_name
+ @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}]
+ remove = @recorder.inverse.first
+ assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove
+ end
+
+ def test_invert_add_index_with_no_options
+ @recorder.record :add_index, [:table, [:one, :two]]
+ remove = @recorder.inverse.first
+ assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
+ end
+
def test_invert_rename_index
- @recorder.record :rename_index, [:old, :new]
+ @recorder.record :rename_index, [:table, :old, :new]
rename = @recorder.inverse.first
- assert_equal [:rename_index, [:new, :old]], rename
+ assert_equal [:rename_index, [:table, :new, :old]], rename
end
def test_invert_add_timestamps
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index bf7565a0d0..93a1249e43 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1071,6 +1071,18 @@ if ActiveRecord::Base.connection.supports_migrations?
Person.connection.drop_table :testings rescue nil
end
+ def test_column_exists_on_table_with_no_options_parameter_supplied
+ Person.connection.create_table :testings do |t|
+ t.string :foo
+ end
+ Person.connection.change_table :testings do |t|
+ assert t.column_exists?(:foo)
+ assert !(t.column_exists?(:bar))
+ end
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
def test_add_table
assert !Reminder.table_exists?
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 8fd1fc2577..4a09a87322 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -182,7 +182,7 @@ class NamedScopeTest < ActiveRecord::TestCase
def test_first_and_last_should_allow_integers_for_limit
assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
- assert_equal Topic.base.last(2), Topic.base.to_a.last(2)
+ assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
end
def test_first_and_last_should_not_use_query_when_results_are_loaded
@@ -456,13 +456,21 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
+ def test_scopes_to_get_newest
+ post = posts(:welcome)
+ old_last_comment = post.comments.newest
+ new_comment = post.comments.create(:body => "My new comment")
+ assert_equal new_comment, post.comments.newest
+ assert_not_equal old_last_comment, post.comments.newest
+ end
+
def test_scopes_are_reset_on_association_reload
post = posts(:welcome)
[:destroy_all, :reset, :delete_all].each do |method|
before = post.comments.containing_the_letter_e
post.association(:comments).send(method)
- assert before.object_id != post.comments.containing_the_letter_e.object_id, "AssociationCollection##{method} should reset the named scopes cache"
+ assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
end
end
@@ -489,14 +497,24 @@ end
class DynamicScopeTest < ActiveRecord::TestCase
fixtures :posts
+ def setup
+ @test_klass = Class.new(Post) do
+ def self.name; "Post"; end
+ end
+ end
+
def test_dynamic_scope
- assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
- assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
+ assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1)
+ assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
end
def test_dynamic_scope_should_create_methods_after_hitting_method_missing
- assert_blank Developer.methods.grep(/scoped_by_created_at/)
- Developer.scoped_by_created_at(nil)
- assert_present Developer.methods.grep(/scoped_by_created_at/)
+ assert_blank @test_klass.methods.grep(/scoped_by_type/)
+ @test_klass.scoped_by_type(nil)
+ assert_present @test_klass.methods.grep(/scoped_by_type/)
+ end
+
+ def test_dynamic_scope_with_less_number_of_arguments
+ assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) }
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 6568eb1d18..67a9ed6cd8 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -755,6 +755,11 @@ module NestedAttributesOnACollectionAssociationTests
Interest.reflect_on_association(:man).options[:inverse_of] = :interests
end
+ def test_can_use_symbols_as_object_identifier
+ @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
+ assert_nothing_raised(NoMethodError) { @pirate.save! }
+ end
+
private
def association_setter
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 57d1441128..adfd8e83a1 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -29,6 +29,26 @@ class PersistencesTest < ActiveRecord::TestCase
end
end
+ def test_update_all_doesnt_ignore_order
+ assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
+ test_update_with_order_succeeds = lambda do |order|
+ begin
+ Author.order(order).update_all('id = id + 1')
+ rescue ActiveRecord::ActiveRecordError
+ false
+ end
+ end
+
+ if test_update_with_order_succeeds.call('id DESC')
+ assert !test_update_with_order_succeeds.call('id ASC') # test that this wasn't a fluke and using an incorrect order results in an exception
+ else
+ # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
+ assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do
+ test_update_with_order_succeeds.call('id DESC')
+ end
+ end
+ end
+
def test_update_all_with_order_and_limit_updates_subset_only
author = authors(:david)
assert_nothing_raised do
@@ -182,9 +202,12 @@ class PersistencesTest < ActiveRecord::TestCase
end
def test_create_columns_not_equal_attributes
- topic = Topic.new
- topic.title = 'Another New Topic'
- topic.send :write_attribute, 'does_not_exist', 'test'
+ topic = Topic.allocate.init_with(
+ 'attributes' => {
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
+ }
+ )
assert_nothing_raised { topic.save }
end
@@ -229,9 +252,11 @@ class PersistencesTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topicReloaded = Topic.find(topic.id)
- topicReloaded.title = "A New Topic"
- topicReloaded.send :write_attribute, 'does_not_exist', 'test'
+ topicReloaded = Topic.allocate
+ topicReloaded.init_with(
+ 'attributes' => topic.attributes.merge('does_not_exist' => 'test')
+ )
+ topicReloaded.title = 'A New Topic'
assert_nothing_raised { topicReloaded.save }
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 379cf5b44e..434b8a677a 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -3,6 +3,8 @@ require "models/project"
require "timeout"
class PooledConnectionsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
def setup
@per_test_teardown = []
@connection = ActiveRecord::Base.remove_connection
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 05a41d8a0a..4bb5752096 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -146,3 +146,23 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key
end
end
+
+class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def test_set_primary_key_with_no_connection
+ return skip("disconnect wipes in-memory db") if in_memory_db?
+
+ connection = ActiveRecord::Base.remove_connection
+
+ model = Class.new(ActiveRecord::Base) do
+ set_primary_key 'foo'
+ end
+
+ assert_equal 'foo', model.primary_key
+
+ ActiveRecord::Base.establish_connection(connection)
+
+ assert_equal 'foo', model.primary_key
+ end
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index a61180cfaf..b2429d631f 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -13,6 +13,32 @@ class QueryCacheTest < ActiveRecord::TestCase
ActiveRecord::Base.connection.disable_query_cache!
end
+ def test_exceptional_middleware_clears_and_disables_cache_on_error
+ assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
+
+ mw = ActiveRecord::QueryCache.new lambda { |env|
+ Task.find 1
+ Task.find 1
+ assert_equal 1, ActiveRecord::Base.connection.query_cache.length
+ raise "lol borked"
+ }
+ assert_raises(RuntimeError) { mw.call({}) }
+
+ assert_equal 0, ActiveRecord::Base.connection.query_cache.length
+ assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
+ end
+
+ def test_exceptional_middleware_leaves_enabled_cache_alone
+ ActiveRecord::Base.connection.enable_query_cache!
+
+ mw = ActiveRecord::QueryCache.new lambda { |env|
+ raise "lol borked"
+ }
+ assert_raises(RuntimeError) { mw.call({}) }
+
+ assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ end
+
def test_middleware_delegates
called = false
mw = ActiveRecord::QueryCache.new lambda { |env|
@@ -121,13 +147,16 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_cache_does_not_wrap_string_results_in_arrays
- require 'sqlite3/version' if current_adapter?(:SQLite3Adapter)
+ if current_adapter?(:SQLite3Adapter)
+ require 'sqlite3/version'
+ sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION
+ end
Task.cache do
# Oracle adapter returns count() as Fixnum or Float
if current_adapter?(:OracleAdapter)
assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter) && SQLite3::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter)
+ elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter)
# Future versions of the sqlite3 adapter will return numeric
assert_instance_of Fixnum,
Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
@@ -203,3 +232,14 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
end
end
+
+class QueryCacheBodyProxyTest < ActiveRecord::TestCase
+
+ test "is polite to it's body and responds to it" do
+ body = Class.new(String) { def to_path; "/path"; end }.new
+ proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body, ActiveRecord::Base.connection_id)
+ assert proxy.respond_to?(:to_path)
+ assert_equal proxy.to_path, "/path"
+ end
+
+end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 97d9669483..69e9fc8d61 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -18,6 +18,7 @@ require 'models/subscriber'
require 'models/subscription'
require 'models/tag'
require 'models/sponsor'
+require 'models/edge'
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -76,7 +77,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_reflection_klass_for_nested_class_name
- reflection = MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil)
+ reflection = MacroReflection.new(:company, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
@@ -216,7 +217,7 @@ class ReflectionTest < ActiveRecord::TestCase
def test_conditions
expected = [
[{ :tags => { :name => 'Blue' } }],
- [{ :taggings => { :comment => 'first' } }, { "taggable_type" => "Post" }],
+ [{ :taggings => { :comment => 'first' } }],
[{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }]
]
actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions
@@ -224,7 +225,7 @@ class ReflectionTest < ActiveRecord::TestCase
expected = [
[{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }],
- [{ "taggable_type" => "Post" }],
+ [],
[]
]
actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions
@@ -244,7 +245,7 @@ class ReflectionTest < ActiveRecord::TestCase
# Normal association
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s
- assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
+ assert_equal "name", Essay.reflect_on_association(:writer).association_primary_key.to_s
# Through association (uses the :primary_key option from the source reflection)
assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s
@@ -252,11 +253,25 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested
end
+ def test_association_primary_key_raises_when_missing_primary_key
+ reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, {}, Author)
+ assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
+
+ through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, {}, Author)
+ through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge'))
+ assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
+ end
+
def test_active_record_primary_key
assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s
assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s
end
+ def test_active_record_primary_key_raises_when_missing_primary_key
+ reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, {}, Edge)
+ assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
+ end
+
def test_foreign_type
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s
@@ -304,13 +319,6 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s
end
- def test_primary_key_name
- assert_deprecated do
- assert_equal "author_id", Author.reflect_on_association(:posts).primary_key_name.to_s
- assert_equal "category_id", Post.reflect_on_association(:categorizations).primary_key_name.to_s
- end
- end
-
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb
index c215602567..1e2093273e 100644
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ b/activerecord/test/cases/relation_scoping_test.rb
@@ -11,6 +11,26 @@ require 'models/reference'
class RelationScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+ def test_reverse_order
+ assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
+ end
+
+ def test_reverse_order_with_arel_node
+ assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order
+ end
+
+ def test_reverse_order_with_multiple_arel_nodes
+ assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order
+ end
+
+ def test_reverse_order_with_arel_nodes_and_strings
+ assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order
+ end
+
+ def test_double_reverse_order_produces_original_order
+ assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order
+ end
+
def test_scoped_find
Developer.where("name = 'David'").scoping do
assert_nothing_raised { Developer.find(1) }
@@ -150,7 +170,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
Developer.where('salary = 80000').scoping do
Developer.limit(10).scoping do
devs = Developer.scoped
- assert_match '(salary = 80000)', devs.arel.to_sql
+ assert_match '(salary = 80000)', devs.to_sql
assert_equal 10, devs.taken
end
end
@@ -312,6 +332,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all
end
+ def test_default_scope_as_class_method_referencing_scope
+ assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_as_block_referencing_scope
+ assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all
+ end
+
def test_default_scope_with_lambda
assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all
end
@@ -456,6 +484,13 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 'Jamis', jamis.name
end
+ # FIXME: I don't know if this is *desired* behavior, but it is *today's*
+ # behavior.
+ def test_create_with_empty_hash_will_not_reset
+ jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
+ assert_equal 'Aaron', jamis.name
+ end
+
def test_unscoped_with_named_scope_should_not_have_default_scope
assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
@@ -463,7 +498,48 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 10, DeveloperCalledJamis.unscoped.poor.length
end
+ def test_default_scope_select_ignored_by_aggregations
+ assert_equal DeveloperWithSelect.all.count, DeveloperWithSelect.count
+ end
+
+ def test_default_scope_select_ignored_by_grouped_aggregations
+ assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }],
+ DeveloperWithSelect.group(:salary).count
+ end
+
def test_default_scope_order_ignored_by_aggregations
assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count
end
+
+ def test_default_scope_find_last
+ assert DeveloperOrderedBySalary.count > 1, "need more than one row for test"
+
+ lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id)
+ assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last
+ end
+
+ def test_default_scope_include_with_count
+ d = DeveloperWithIncludes.create!
+ d.audit_logs.create! :message => 'foo'
+
+ assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
+ end
+
+ def test_default_scope_is_threadsafe
+ if in_memory_db?
+ skip "in memory db can't share a db between threads"
+ end
+
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.count
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.count
+ end
+ threads.each(&:join)
+ end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 6874bd18f8..b23ead6feb 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -20,7 +20,7 @@ module ActiveRecord
end
def test_single_values
- assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder].map(&:to_s).sort,
+ assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order].map(&:to_s).sort,
Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index fc9df8c7a3..95408a5f29 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -145,6 +145,18 @@ class RelationTest < ActiveRecord::TestCase
assert_equal topics(:first).title, topics.first.title
end
+
+ def test_finding_with_arel_order
+ topics = Topic.order(Topic.arel_table[:id].asc)
+ assert_equal 4, topics.to_a.size
+ assert_equal topics(:first).title, topics.first.title
+ end
+
+ def test_finding_last_with_arel_order
+ topics = Topic.order(Topic.arel_table[:id].asc)
+ assert_equal topics(:fourth).title, topics.last.title
+ end
+
def test_finding_with_order_concatenated
topics = Topic.order('author_name').order('title')
assert_equal 4, topics.to_a.size
@@ -164,6 +176,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal entrants(:first).name, entrants.first.name
end
+ def test_finding_with_cross_table_order_and_limit
+ tags = Tag.includes(:taggings).
+ order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").
+ limit(1).to_a
+ assert_equal 1, tags.length
+ end
+
def test_finding_with_complex_order_and_limit
tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
assert_equal 1, tags.length
@@ -372,6 +391,15 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_dynamic_find_by_attributes_should_yield_found_object
+ david = authors(:david)
+ yielded_value = nil
+ Author.find_by_name(david.name) do |author|
+ yielded_value = author
+ end
+ assert_equal david, yielded_value
+ end
+
def test_dynamic_find_by_attributes
david = authors(:david)
author = Author.preload(:taggings).find_by_id(david.id)
@@ -512,6 +540,29 @@ class RelationTest < ActiveRecord::TestCase
}
end
+ def test_find_all_using_where_with_relation_and_alternate_primary_key
+ cool_first = minivans(:cool_first)
+ # switching the lines below would succeed in current rails
+ # assert_queries(2) {
+ assert_queries(1) {
+ relation = Minivan.where(:minivan_id => Minivan.where(:name => cool_first.name))
+ assert_equal [cool_first], relation.all
+ }
+ end
+
+ def test_find_all_using_where_with_relation_does_not_alter_select_values
+ david = authors(:david)
+
+ subquery = Author.where(:id => david.id)
+
+ assert_queries(1) {
+ relation = Author.where(:id => subquery)
+ assert_equal [david], relation.all
+ }
+
+ assert_equal 0, subquery.select_values.size
+ end
+
def test_find_all_using_where_with_relation_with_joins
david = authors(:david)
assert_queries(1) {
@@ -812,6 +863,128 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 'hen', hen.name
end
+ def test_first_or_create
+ parrot = Bird.where(:color => 'green').first_or_create(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+
+ same_parrot = Bird.where(:color => 'green').first_or_create(:name => 'parakeet')
+ assert_kind_of Bird, same_parrot
+ assert same_parrot.persisted?
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_with_no_parameters
+ parrot = Bird.where(:color => 'green').first_or_create
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_create_with_block
+ parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+
+ same_parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parakeet' }
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_with_array
+ several_green_birds = Bird.where(:color => 'green').first_or_create([{:name => 'parrot'}, {:name => 'parakeet'}])
+ assert_kind_of Array, several_green_birds
+ several_green_birds.each { |bird| assert bird.persisted? }
+
+ same_parrot = Bird.where(:color => 'green').first_or_create([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ assert_kind_of Bird, same_parrot
+ assert_equal several_green_birds.first, same_parrot
+ end
+
+ def test_first_or_create_bang_with_valid_options
+ parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+
+ same_parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parakeet')
+ assert_kind_of Bird, same_parrot
+ assert same_parrot.persisted?
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_bang_with_invalid_options
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!(:pirate_id => 1) }
+ end
+
+ def test_first_or_create_bang_with_no_parameters
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create! }
+ end
+
+ def test_first_or_create_bang_with_valid_block
+ parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+
+ same_parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parakeet' }
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_bang_with_invalid_block
+ assert_raise(ActiveRecord::RecordInvalid) do
+ Bird.where(:color => 'green').first_or_create! { |bird| bird.pirate_id = 1 }
+ end
+ end
+
+ def test_first_or_create_with_valid_array
+ several_green_birds = Bird.where(:color => 'green').first_or_create!([{:name => 'parrot'}, {:name => 'parakeet'}])
+ assert_kind_of Array, several_green_birds
+ several_green_birds.each { |bird| assert bird.persisted? }
+
+ same_parrot = Bird.where(:color => 'green').first_or_create!([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ assert_kind_of Bird, same_parrot
+ assert_equal several_green_birds.first, same_parrot
+ end
+
+ def test_first_or_create_with_invalid_array
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!([ {:name => 'parrot'}, {:pirate_id => 1} ]) }
+ end
+
+ def test_first_or_initialize
+ parrot = Bird.where(:color => 'green').first_or_initialize(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_initialize_with_no_parameters
+ parrot = Bird.where(:color => 'green').first_or_initialize
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert !parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_initialize_with_block
+ parrot = Bird.where(:color => 'green').first_or_initialize { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+ end
+
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name
@@ -933,4 +1106,46 @@ class RelationTest < ActiveRecord::TestCase
assert scope.eager_loading?
end
+
+ def test_ordering_with_extra_spaces
+ assert_equal authors(:david), Author.order('id DESC , name DESC').last
+ end
+
+ def test_update_all_with_joins
+ comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id)
+ count = comments.count
+
+ assert_equal count, comments.update_all(:post_id => posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ end
+
+ def test_update_all_with_joins_and_limit
+ comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).limit(1)
+ assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
+ end
+
+ def test_update_all_with_joins_and_limit_and_order
+ comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('comments.id').limit(1)
+ assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ assert_equal posts(:welcome), comments(:more_greetings).post
+ end
+
+ def test_update_all_with_joins_and_offset
+ all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id)
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
+ end
+
+ def test_update_all_with_joins_and_offset_and_order
+ all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('posts.id', 'comments.id')
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:more_greetings).post
+ assert_equal posts(:welcome), comments(:greetings).post
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index e8f2f44189..71ff727b7f 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,13 +1,22 @@
require "cases/helper"
-require 'stringio'
class SchemaDumperTest < ActiveRecord::TestCase
+ def setup
+ @stream = StringIO.new
+ end
+
def standard_dump
- stream = StringIO.new
+ @stream = StringIO.new
ActiveRecord::SchemaDumper.ignore_tables = []
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- stream.string
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream)
+ @stream.string
+ end
+
+ if "string".encoding_aware?
+ def test_magic_comment
+ assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
+ end
end
def test_schema_dump
@@ -230,4 +239,3 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved"
end
end
-
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 677d659f39..61b04b3e37 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,26 +1,20 @@
require "cases/helper"
require 'models/contact'
require 'models/topic'
-require 'models/reply'
-require 'models/company'
class SerializationTest < ActiveRecord::TestCase
-
- fixtures :topics, :companies, :accounts
-
FORMATS = [ :xml, :json ]
def setup
@contact_attributes = {
- :name => 'aaron stack',
- :age => 25,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => false,
- :preferences => { :gem => '<strong>ruby</strong>' }
+ :name => 'aaron stack',
+ :age => 25,
+ :avatar => 'binarydata',
+ :created_at => Time.utc(2006, 8, 1),
+ :awesome => false,
+ :preferences => { :gem => '<strong>ruby</strong>' },
+ :alternative_id => nil
}
-
- @contact = Contact.new(@contact_attributes)
end
def test_serialized_init_with
@@ -29,134 +23,6 @@ class SerializationTest < ActiveRecord::TestCase
assert_equal 'foo', topic.content
end
- def test_to_xml
- xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
- bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
- written_on_in_current_timezone = topics(:first).written_on.xmlschema
- last_read_in_current_timezone = topics(:first).last_read.xmlschema
-
- assert_equal "topic", xml.root.name
- assert_equal "The First Topic" , xml.elements["//title"].text
- assert_equal "David" , xml.elements["//author-name"].text
- assert_match "Have a nice day", xml.elements["//content"].text
-
- assert_equal "1", xml.elements["//id"].text
- assert_equal "integer" , xml.elements["//id"].attributes['type']
-
- assert_equal "1", xml.elements["//replies-count"].text
- assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
-
- assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
- assert_equal "datetime" , xml.elements["//written-on"].attributes['type']
-
- assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
-
- assert_equal nil, xml.elements["//parent-id"].text
- assert_equal "integer", xml.elements["//parent-id"].attributes['type']
- assert_equal "true", xml.elements["//parent-id"].attributes['nil']
-
- if current_adapter?(:SybaseAdapter)
- assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
- assert_equal "datetime" , xml.elements["//last-read"].attributes['type']
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
- end
-
- # Oracle and DB2 don't have true boolean or time-only fields
- unless current_adapter?(:OracleAdapter, :DB2Adapter)
- assert_equal "false", xml.elements["//approved"].text
- assert_equal "boolean" , xml.elements["//approved"].attributes['type']
-
- assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
- assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type']
- end
- end
-
- def test_to_xml_skipping_attributes
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
- assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
-
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
- end
-
- def test_to_xml_including_has_many_association
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
- assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<replies type="array"><reply>))
- assert xml.include?(%(<title>The Second Topic of the day</title>))
- end
-
- def test_array_to_xml_including_has_many_association
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
- assert xml.include?(%(<replies type="array"><reply>))
- end
-
- def test_array_to_xml_including_methods
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
- assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
- assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
- end
-
- def test_array_to_xml_including_has_one_association
- xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
- assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
- end
-
- def test_array_to_xml_including_belongs_to_association
- xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- end
-
- def test_to_xml_including_belongs_to_association
- xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert !xml.include?("<firm>")
-
- xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?("<firm>")
- end
-
- def test_to_xml_including_multiple_associations
- xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<account>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_to_xml_including_multiple_associations_with_options
- xml = companies(:first_firm).to_xml(
- :indent => 0, :skip_instruct => true,
- :include => { :clients => { :only => :name } }
- )
-
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_to_xml_including_methods
- xml = Company.new.to_xml(:methods => :arbitrary_method, :skip_instruct => true)
- assert_equal "<company>", xml.first(9)
- assert xml.include?(%(<arbitrary-method>I am Jack's profound disappointment</arbitrary-method>))
- end
-
- def test_to_xml_with_block
- value = "Rockin' the block"
- xml = Company.new.to_xml(:skip_instruct => true) do |_xml|
- _xml.tag! "arbitrary-element", value
- end
- assert_equal "<company>", xml.first(9)
- assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
- end
-
def test_serialize_should_be_reversible
for format in FORMATS
@serialized = Contact.new.send("to_#{format}")
@@ -184,11 +50,4 @@ class SerializationTest < ActiveRecord::TestCase
assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}"
end
end
-
- def test_serialize_should_xml_skip_instruct_for_included_records
- @contact.alternative = Contact.new(:name => 'Copa Cabana')
- @serialized = @contact.to_xml(:include => [ :alternative ])
- assert_equal @serialized.index('<?xml '), 0
- assert_nil @serialized.index('<?xml ', 1)
- end
end
diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb
index 669c0b7b4d..258cee7aba 100644
--- a/activerecord/test/cases/session_store/session_test.rb
+++ b/activerecord/test/cases/session_store/session_test.rb
@@ -36,6 +36,7 @@ module ActiveRecord
end
def test_find_by_sess_id_compat
+ Session.reset_column_information
klass = Class.new(Session) do
def self.session_id_column
'sessid'
@@ -53,6 +54,7 @@ module ActiveRecord
assert_equal session.sessid, found.session_id
ensure
klass.drop_table!
+ Session.reset_column_information
end
def test_find_by_session_id
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index ceb1452afd..4445a12e1d 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase
def setup
@developer = Developer.first
+ @developer.update_attribute(:updated_at, Time.now.prev_month)
@previously_updated_at = @developer.updated_at
end
@@ -40,6 +41,15 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal previous_salary, @developer.salary
end
+ def test_touching_a_record_with_default_scope_that_excludes_it_updates_its_timestamp
+ developer = @developer.becomes(DeveloperCalledJamis)
+
+ developer.touch
+ assert_not_equal @previously_updated_at, developer.updated_at
+ developer.reload
+ assert_not_equal @previously_updated_at, developer.updated_at
+ end
+
def test_saving_when_record_timestamps_is_false_doesnt_update_its_timestamp
Developer.record_timestamps = false
@developer.name = "John Smith"
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index f85fb4e5da..e82ca3f93d 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base
end
class TestUnconnectedAdapter < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_fixtures = false
def setup
@underlying = ActiveRecord::Base.connection
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 756c8a32eb..88751a72f9 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -5,6 +5,9 @@ require 'models/author'
require 'models/comment'
require 'models/company_in_module'
require 'models/toy'
+require 'models/topic'
+require 'models/reply'
+require 'models/company'
class XmlSerializationTest < ActiveRecord::TestCase
def test_should_serialize_default_root
@@ -50,6 +53,23 @@ class XmlSerializationTest < ActiveRecord::TestCase
end
assert_match %r{<creator>David</creator>}, @xml
end
+
+ def test_to_xml_with_block
+ value = "Rockin' the block"
+ xml = Contact.new.to_xml(:skip_instruct => true) do |_xml|
+ _xml.tag! "arbitrary-element", value
+ end
+ assert_equal "<contact>", xml.first(9)
+ assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
+ end
+
+ def test_should_skip_instruct_for_included_records
+ @contact = Contact.new
+ @contact.alternative = Contact.new(:name => 'Copa Cabana')
+ @xml = @contact.to_xml(:include => [ :alternative ])
+ assert_equal @xml.index('<?xml '), 0
+ assert_nil @xml.index('<?xml ', 1)
+ end
end
class DefaultXmlSerializationTest < ActiveRecord::TestCase
@@ -148,7 +168,63 @@ class NilXmlSerializationTest < ActiveRecord::TestCase
end
class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :projects
+ fixtures :topics, :companies, :accounts, :authors, :posts, :projects
+
+ def test_to_xml
+ xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
+ bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
+ written_on_in_current_timezone = topics(:first).written_on.xmlschema
+ last_read_in_current_timezone = topics(:first).last_read.xmlschema
+
+ assert_equal "topic", xml.root.name
+ assert_equal "The First Topic" , xml.elements["//title"].text
+ assert_equal "David" , xml.elements["//author-name"].text
+ assert_match "Have a nice day", xml.elements["//content"].text
+
+ assert_equal "1", xml.elements["//id"].text
+ assert_equal "integer" , xml.elements["//id"].attributes['type']
+
+ assert_equal "1", xml.elements["//replies-count"].text
+ assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
+
+ assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
+ assert_equal "datetime" , xml.elements["//written-on"].attributes['type']
+
+ assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
+
+ assert_equal nil, xml.elements["//parent-id"].text
+ assert_equal "integer", xml.elements["//parent-id"].attributes['type']
+ assert_equal "true", xml.elements["//parent-id"].attributes['nil']
+
+ if current_adapter?(:SybaseAdapter)
+ assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
+ assert_equal "datetime" , xml.elements["//last-read"].attributes['type']
+ else
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_equal "2004-04-15", xml.elements["//last-read"].text
+ assert_equal "date" , xml.elements["//last-read"].attributes['type']
+ end
+
+ # Oracle and DB2 don't have true boolean or time-only fields
+ unless current_adapter?(:OracleAdapter, :DB2Adapter)
+ assert_equal "false", xml.elements["//approved"].text
+ assert_equal "boolean" , xml.elements["//approved"].attributes['type']
+
+ assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
+ assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type']
+ end
+ end
+
+ def test_except_option
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
+ assert_equal "<topic>", xml.first(7)
+ assert !xml.include?(%(<title>The First Topic</title>))
+ assert xml.include?(%(<author-name>David</author-name>))
+
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
+ assert !xml.include?(%(<title>The First Topic</title>))
+ assert !xml.include?(%(<author-name>David</author-name>))
+ end
# to_xml used to mess with the hash the user provided which
# caused the builder to be reused. This meant the document kept
@@ -184,6 +260,39 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<hello-post>}, xml
end
+ def test_including_has_many_association
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
+ assert_equal "<topic>", xml.first(7)
+ assert xml.include?(%(<replies type="array"><reply>))
+ assert xml.include?(%(<title>The Second Topic of the day</title>))
+ end
+
+ def test_including_belongs_to_association
+ xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert !xml.include?("<firm>")
+
+ xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert xml.include?("<firm>")
+ end
+
+ def test_including_multiple_associations
+ xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
+ assert_equal "<firm>", xml.first(6)
+ assert xml.include?(%(<account>))
+ assert xml.include?(%(<clients type="array"><client>))
+ end
+
+ def test_including_association_with_options
+ xml = companies(:first_firm).to_xml(
+ :indent => 0, :skip_instruct => true,
+ :include => { :clients => { :only => :name } }
+ )
+
+ assert_equal "<firm>", xml.first(6)
+ assert xml.include?(%(<client><name>Summit</name></client>))
+ assert xml.include?(%(<clients type="array"><client>))
+ end
+
def test_methods_are_called_on_object
xml = authors(:david).to_xml :methods => :label, :indent => 0
assert_match %r{<label>.*</label>}, xml
@@ -265,4 +374,27 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size
end
+ def test_array_to_xml_including_has_many_association
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
+ assert xml.include?(%(<replies type="array"><reply>))
+ end
+
+ def test_array_to_xml_including_methods
+ xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
+ assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
+ assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
+ end
+
+ def test_array_to_xml_including_has_one_association
+ xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
+ assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
+ end
+
+ def test_array_to_xml_including_belongs_to_association
+ xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
+ assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
new file mode 100644
index 0000000000..f450efd839
--- /dev/null
+++ b/activerecord/test/config.example.yml
@@ -0,0 +1,138 @@
+default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
+
+connections:
+ jdbcderby:
+ arunit: activerecord_unittest
+ arunit2: activerecord_unittest2
+
+ jdbch2:
+ arunit: activerecord_unittest
+ arunit2: activerecord_unittest2
+
+ jdbchsqldb:
+ arunit: activerecord_unittest
+ arunit2: activerecord_unittest2
+
+ jdbcmysql:
+ arunit:
+ username: rails
+ encoding: utf8
+ arunit2:
+ username: rails
+ encoding: utf8
+
+ jdbcpostgresql:
+ arunit:
+ username: <%= ENV['user'] || 'rails' %>
+ arunit2:
+ username: <%= ENV['user'] || 'rails' %>
+
+ jdbcsqlite3:
+ arunit:
+ database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
+ timeout: 5000
+ arunit2:
+ database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
+ timeout: 5000
+
+ db2:
+ arunit:
+ adapter: ibm_db
+ host: localhost
+ username: arunit
+ password: arunit
+ database: arunit
+ arunit2:
+ adapter: ibm_db
+ host: localhost
+ username: arunit
+ password: arunit
+ database: arunit2
+
+ firebird:
+ arunit:
+ host: localhost
+ username: rails
+ password: rails
+ charset: UTF8
+ arunit2:
+ host: localhost
+ username: rails
+ password: rails
+ charset: UTF8
+
+ frontbase:
+ arunit:
+ host: localhost
+ username: rails
+ session_name: unittest-<%= $$ %>
+ arunit2:
+ host: localhost
+ username: rails
+ session_name: unittest-<%= $$ %>
+
+ mysql:
+ arunit:
+ username: rails
+ encoding: utf8
+ arunit2:
+ username: rails
+ encoding: utf8
+
+ mysql2:
+ arunit:
+ username: rails
+ encoding: utf8
+ arunit2:
+ username: rails
+ encoding: utf8
+
+ openbase:
+ arunit:
+ username: admin
+ arunit2:
+ username: admin
+
+ oracle:
+ arunit:
+ adapter: oracle_enhanced
+ database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %>
+ username: <%= ENV['ARUNIT_USER_NAME'] || 'arunit' %>
+ password: <%= ENV['ARUNIT_PASSWORD'] || 'arunit' %>
+ emulate_oracle_adapter: true
+ arunit2:
+ adapter: oracle_enhanced
+ database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %>
+ username: <%= ENV['ARUNIT2_USER_NAME'] || 'arunit2' %>
+ password: <%= ENV['ARUNIT2_PASSWORD'] || 'arunit2' %>
+ emulate_oracle_adapter: true
+
+ postgresql:
+ arunit:
+ min_messages: warning
+ arunit2:
+ min_messages: warning
+
+ sqlite3:
+ arunit:
+ database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
+ timeout: 5000
+ arunit2:
+ database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
+ timeout: 5000
+
+ sqlite3_mem:
+ arunit:
+ adapter: sqlite3
+ database: ':memory:'
+ arunit2:
+ adapter: sqlite3
+ database: ':memory:'
+
+ sybase:
+ arunit:
+ host: database_ASE
+ username: sa
+ arunit2:
+ host: database_ASE
+ username: sa
diff --git a/activerecord/test/connections/jdbc_jdbcderby/connection.rb b/activerecord/test/connections/jdbc_jdbcderby/connection.rb
deleted file mode 100644
index 222ef5db38..0000000000
--- a/activerecord/test/connections/jdbc_jdbcderby/connection.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-print "Using Derby via JRuby, activerecord-jdbc-adapter and activerecord-jdbcderby-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'jdbcderby',
- :database => 'activerecord_unittest'
- },
- 'arunit2' => {
- :adapter => 'jdbcderby',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/jdbc_jdbch2/connection.rb b/activerecord/test/connections/jdbc_jdbch2/connection.rb
deleted file mode 100644
index 9d2875e8e7..0000000000
--- a/activerecord/test/connections/jdbc_jdbch2/connection.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-print "Using H2 via JRuby, activerecord-jdbc-adapter and activerecord-jdbch2-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'jdbch2',
- :database => 'activerecord_unittest'
- },
- 'arunit2' => {
- :adapter => 'jdbch2',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb b/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb
deleted file mode 100644
index fa943c2c76..0000000000
--- a/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-print "Using HSQLDB via JRuby, activerecord-jdbc-adapter and activerecord-jdbchsqldb-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'jdbchsqldb',
- :database => 'activerecord_unittest'
- },
- 'arunit2' => {
- :adapter => 'jdbchsqldb',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/jdbc_jdbcmysql/connection.rb b/activerecord/test/connections/jdbc_jdbcmysql/connection.rb
deleted file mode 100644
index e2517a50eb..0000000000
--- a/activerecord/test/connections/jdbc_jdbcmysql/connection.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-print "Using MySQL via JRuby, activerecord-jdbc-adapter and activerecord-jdbcmysql-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
-# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'jdbcmysql',
- :username => 'rails',
- :encoding => 'utf8',
- :database => 'activerecord_unittest',
- },
- 'arunit2' => {
- :adapter => 'jdbcmysql',
- :username => 'rails',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
-
diff --git a/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb b/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb
deleted file mode 100644
index 0685da4433..0000000000
--- a/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-print "Using Postgrsql via JRuby, activerecord-jdbc-adapter and activerecord-postgresql-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-# createuser rails --createdb --no-superuser --no-createrole
-# createdb -O rails activerecord_unittest
-# createdb -O rails activerecord_unittest2
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'jdbcpostgresql',
- :username => ENV['USER'] || 'rails',
- :database => 'activerecord_unittest'
- },
- 'arunit2' => {
- :adapter => 'jdbcpostgresql',
- :username => ENV['USER'] || 'rails',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
-
diff --git a/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb b/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb
deleted file mode 100644
index 26d4676ff3..0000000000
--- a/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Using SQLite3 via JRuby, activerecord-jdbc-adapter and activerecord-jdbcsqlite3-adapter\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-class SqliteError < StandardError
-end
-
-BASE_DIR = FIXTURES_ROOT
-sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3"
-sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3"
-
-def make_connection(clazz, db_file)
- ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'jdbcsqlite3', :database => db_file, :timeout => 5000 } }
- unless File.exist?(db_file)
- puts "SQLite3 database not found at #{db_file}. Rebuilding it."
- sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"}
- puts "Executing '#{sqlite_command}'"
- raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command)
- end
- clazz.establish_connection(clazz.name)
-end
-
-make_connection(ActiveRecord::Base, sqlite_test_db)
-make_connection(Course, sqlite_test_db2)
diff --git a/activerecord/test/connections/native_db2/connection.rb b/activerecord/test/connections/native_db2/connection.rb
deleted file mode 100644
index 324315d2c8..0000000000
--- a/activerecord/test/connections/native_db2/connection.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Using native DB2\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'db2',
- :host => 'localhost',
- :username => 'arunit',
- :password => 'arunit',
- :database => 'arunit'
- },
- 'arunit2' => {
- :adapter => 'db2',
- :host => 'localhost',
- :username => 'arunit',
- :password => 'arunit',
- :database => 'arunit2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_firebird/connection.rb b/activerecord/test/connections/native_firebird/connection.rb
deleted file mode 100644
index 67a936ca97..0000000000
--- a/activerecord/test/connections/native_firebird/connection.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-print "Using native Firebird\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'firebird',
- :host => 'localhost',
- :username => 'rails',
- :password => 'rails',
- :database => 'activerecord_unittest',
- :charset => 'UTF8'
- },
- 'arunit2' => {
- :adapter => 'firebird',
- :host => 'localhost',
- :username => 'rails',
- :password => 'rails',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_frontbase/connection.rb b/activerecord/test/connections/native_frontbase/connection.rb
deleted file mode 100644
index c01d864a8b..0000000000
--- a/activerecord/test/connections/native_frontbase/connection.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-puts 'Using native Frontbase'
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'frontbase',
- :host => 'localhost',
- :username => 'rails',
- :password => '',
- :database => 'activerecord_unittest',
- :session_name => "unittest-#{$$}"
- },
- 'arunit2' => {
- :adapter => 'frontbase',
- :host => 'localhost',
- :username => 'rails',
- :password => '',
- :database => 'activerecord_unittest2',
- :session_name => "unittest-#{$$}"
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
deleted file mode 100644
index 140e06d631..0000000000
--- a/activerecord/test/connections/native_mysql/connection.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Using native MySQL\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
-# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'mysql',
- :username => 'rails',
- :encoding => 'utf8',
- :database => 'activerecord_unittest',
- },
- 'arunit2' => {
- :adapter => 'mysql',
- :username => 'rails',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_mysql2/connection.rb b/activerecord/test/connections/native_mysql2/connection.rb
deleted file mode 100644
index c6f198b1ac..0000000000
--- a/activerecord/test/connections/native_mysql2/connection.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Using native Mysql2\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
-# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'mysql2',
- :username => 'rails',
- :encoding => 'utf8',
- :database => 'activerecord_unittest',
- },
- 'arunit2' => {
- :adapter => 'mysql2',
- :username => 'rails',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_openbase/connection.rb b/activerecord/test/connections/native_openbase/connection.rb
deleted file mode 100644
index 655cb9ca26..0000000000
--- a/activerecord/test/connections/native_openbase/connection.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-print "Using native OpenBase\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'openbase',
- :username => 'admin',
- :database => 'activerecord_unittest',
- },
- 'arunit2' => {
- :adapter => 'openbase',
- :username => 'admin',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb
deleted file mode 100644
index 99f921879c..0000000000
--- a/activerecord/test/connections/native_oracle/connection.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# uses oracle_enhanced adapter in ENV['ORACLE_ENHANCED_PATH'] or from github.com/rsim/oracle-enhanced.git
-require 'active_record/connection_adapters/oracle_enhanced_adapter'
-
-# otherwise failed with silence_warnings method missing exception
-require 'active_support/core_ext/kernel/reporting'
-
-print "Using Oracle\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-# Set these to your database connection strings
-ENV['ARUNIT_DB_NAME'] ||= 'orcl'
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'oracle_enhanced',
- :database => ENV['ARUNIT_DB_NAME'],
- :username => 'arunit',
- :password => 'arunit',
- :emulate_oracle_adapter => true
- },
- 'arunit2' => {
- :adapter => 'oracle_enhanced',
- :database => ENV['ARUNIT_DB_NAME'],
- :username => 'arunit2',
- :password => 'arunit2',
- :emulate_oracle_adapter => true
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
-
diff --git a/activerecord/test/connections/native_postgresql/connection.rb b/activerecord/test/connections/native_postgresql/connection.rb
deleted file mode 100644
index 3b5ff90003..0000000000
--- a/activerecord/test/connections/native_postgresql/connection.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-print "Using native PostgreSQL\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'postgresql',
- :database => 'activerecord_unittest',
- :min_messages => 'warning'
- },
- 'arunit2' => {
- :adapter => 'postgresql',
- :database => 'activerecord_unittest2',
- :min_messages => 'warning'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/connections/native_sqlite3/connection.rb b/activerecord/test/connections/native_sqlite3/connection.rb
deleted file mode 100644
index c2aff5551f..0000000000
--- a/activerecord/test/connections/native_sqlite3/connection.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-print "Using native SQLite3\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-BASE_DIR = FIXTURES_ROOT
-sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3"
-sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3"
-
-def make_connection(clazz, db_file)
- ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } }
- clazz.establish_connection(clazz.name)
-end
-
-make_connection(ActiveRecord::Base, sqlite_test_db)
-make_connection(Course, sqlite_test_db2)
diff --git a/activerecord/test/connections/native_sqlite3_mem/connection.rb b/activerecord/test/connections/native_sqlite3_mem/connection.rb
deleted file mode 100644
index 14e10900d1..0000000000
--- a/activerecord/test/connections/native_sqlite3_mem/connection.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file connects to an in-memory SQLite3 database, which is a very fast way to run the tests.
-# The downside is that disconnect from the database results in the database effectively being
-# wiped. For this reason, pooled_connections_test.rb is disabled when using an in-memory database.
-
-print "Using native SQLite3 (in memory)\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-class SqliteError < StandardError
-end
-
-def make_connection(clazz)
- ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => ':memory:' } }
- clazz.establish_connection(clazz.name)
-end
-
-make_connection(ActiveRecord::Base)
-make_connection(Course)
diff --git a/activerecord/test/connections/native_sybase/connection.rb b/activerecord/test/connections/native_sybase/connection.rb
deleted file mode 100644
index 3282d26922..0000000000
--- a/activerecord/test/connections/native_sybase/connection.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-print "Using native Sybase Open Client\n"
-require_dependency 'models/course'
-require 'logger'
-
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-ActiveRecord::Base.configurations = {
- 'arunit' => {
- :adapter => 'sybase',
- :host => 'database_ASE',
- :username => 'sa',
- :database => 'activerecord_unittest'
- },
- 'arunit2' => {
- :adapter => 'sybase',
- :host => 'database_ASE',
- :username => 'sa',
- :database => 'activerecord_unittest2'
- }
-}
-
-ActiveRecord::Base.establish_connection 'arunit'
-Course.establish_connection 'arunit2'
diff --git a/activerecord/test/fixtures/categories_ordered.yml b/activerecord/test/fixtures/categories_ordered.yml
index 2afc6cb5a9..294a6368d6 100644
--- a/activerecord/test/fixtures/categories_ordered.yml
+++ b/activerecord/test/fixtures/categories_ordered.yml
@@ -1,4 +1,4 @@
---- !!omap
+--- !omap
<% 100.times do |i| %>
- fixture_no_<%= i %>:
id: <%= i %>
diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml
index 60eb641054..a5d52bd438 100644
--- a/activerecord/test/fixtures/memberships.yml
+++ b/activerecord/test/fixtures/memberships.yml
@@ -25,3 +25,10 @@ blarpy_winkup_crazy_club:
member_id: 3
favourite: false
type: CurrentMembership
+
+selected_membership_of_boring_club:
+ joined_on: <%= 3.weeks.ago.to_s(:db) %>
+ club: boring_club
+ member_id: 1
+ favourite: false
+ type: SelectedMembership
diff --git a/activerecord/test/fixtures/parrots.yml b/activerecord/test/fixtures/parrots.yml
index 8b73b8cdf6..8425ef98e0 100644
--- a/activerecord/test/fixtures/parrots.yml
+++ b/activerecord/test/fixtures/parrots.yml
@@ -24,4 +24,4 @@ DEFAULTS: &DEFAULTS
parrot_sti_class: LiveParrot
davey:
- <<: *DEFAULTS
+ *DEFAULTS
diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml
index abb91101da..6004f390a4 100644
--- a/activerecord/test/fixtures/pirates.yml
+++ b/activerecord/test/fixtures/pirates.yml
@@ -5,5 +5,5 @@ blackbeard:
redbeard:
catchphrase: "Avast!"
parrot: louis
- created_on: <%= 2.weeks.ago.to_s(:db) %>
- updated_on: <%= 2.weeks.ago.to_s(:db) %>
+ created_on: "<%= 2.weeks.ago.to_s(:db) %>"
+ updated_on: "<%= 2.weeks.ago.to_s(:db) %>"
diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml
index 01c95b3a4c..402ca85faf 100644
--- a/activerecord/test/fixtures/tasks.yml
+++ b/activerecord/test/fixtures/tasks.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
first_task:
id: 1
starting: 2005-03-30t06:30:00.00+01:00
diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb
new file mode 100644
index 0000000000..1f35ef45da
--- /dev/null
+++ b/activerecord/test/models/aircraft.rb
@@ -0,0 +1,4 @@
+class Aircraft < ActiveRecord::Base
+ self.pluralize_table_names = false
+ has_many :engines, :foreign_key => "car_id"
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index e0cbc44265..23db5650d4 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -138,6 +138,9 @@ class Author < ActiveRecord::Base
has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2,
:conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }
+ has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude'
+ has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments
+
scope :relation_include_posts, includes(:posts)
scope :relation_include_tags, includes(:tags)
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 643dcefed3..888afc7604 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -4,11 +4,38 @@ class Bulb < ActiveRecord::Base
attr_protected :car_id, :frickinawesome
- attr_reader :scope_after_initialize
+ attr_reader :scope_after_initialize, :attributes_after_initialize
after_initialize :record_scope_after_initialize
def record_scope_after_initialize
@scope_after_initialize = self.class.scoped
end
+ after_initialize :record_attributes_after_initialize
+ def record_attributes_after_initialize
+ @attributes_after_initialize = attributes.dup
+ end
+
+ def color=(color)
+ self[:color] = color.upcase + "!"
+ end
+
+ def self.new(attributes = {}, options = {}, &block)
+ bulb_type = (attributes || {}).delete(:bulb_type)
+
+ if options && options[:as] == :admin && bulb_type.present?
+ bulb_class = "#{bulb_type.to_s.camelize}Bulb".constantize
+ bulb_class.new(attributes, options, &block)
+ else
+ super
+ end
+ end
+end
+
+class CustomBulb < Bulb
+ after_initialize :set_awesomeness
+
+ def set_awesomeness
+ self.frickinawesome = true if name == 'Dude'
+ end
end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index de1864345a..b9c2e8ec9a 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -4,8 +4,11 @@ class Car < ActiveRecord::Base
has_many :foo_bulbs, :class_name => "Bulb", :conditions => { :name => 'foo' }
has_many :frickinawesome_bulbs, :class_name => "Bulb", :conditions => { :frickinawesome => true }
+ has_one :bulb
+ has_one :frickinawesome_bulb, :class_name => "Bulb", :conditions => { :frickinawesome => true }
+
has_many :tyres
- has_many :engines
+ has_many :engines, :dependent => :destroy
has_many :wheels, :as => :wheelable
scope :incl_tyres, includes(:tyres)
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 2a4c37089a..88b139d931 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -6,10 +6,14 @@ class Comment < ActiveRecord::Base
scope :for_first_author,
:joins => :post,
:conditions => { "posts.author_id" => 1 }
+ scope :created
belongs_to :post, :counter_cache => true
has_many :ratings
+ has_many :children, :class_name => 'Comment', :foreign_key => :parent_id
+ belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count
+
def self.what_are_you
'a comment...'
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index e0b30efd51..c1f7a4171a 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -124,6 +124,18 @@ class Client < Company
has_many :accounts, :through => :firm
belongs_to :account
+ class RaisedOnSave < RuntimeError; end
+ attr_accessor :raise_on_save
+ before_save do
+ raise RaisedOnSave if raise_on_save
+ end
+
+ class RaisedOnDestroy < RuntimeError; end
+ attr_accessor :raise_on_destroy
+ before_destroy do
+ raise RaisedOnDestroy if raise_on_destroy
+ end
+
# Record destruction so we can test whether firm.clients.clear has
# is calling client.destroy, deleting from the database, or setting
# foreign keys to NULL.
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index e081eee661..3d15c7fbed 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -11,12 +11,13 @@ class Contact < ActiveRecord::Base
connection.merge_column('contacts', name, sql_type, options)
end
- column :name, :string
- column :age, :integer
- column :avatar, :binary
- column :created_at, :datetime
- column :awesome, :boolean
- column :preferences, :string
+ column :name, :string
+ column :age, :integer
+ column :avatar, :binary
+ column :created_at, :datetime
+ column :awesome, :boolean
+ column :preferences, :string
+ column :alternative_id, :integer
serialize :preferences
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 94fd48e12a..2cf5aa7a85 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,4 +1,19 @@
class Contract < ActiveRecord::Base
belongs_to :company
belongs_to :developer
+
+ before_save :hi
+ after_save :bye
+
+ attr_accessor :hi_count, :bye_count
+
+ def hi
+ @hi_count ||= 0
+ @hi_count += 1
+ end
+
+ def bye
+ @bye_count ||= 0
+ @bye_count += 1
+ end
end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 152f804e16..4dc9fff9fd 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -86,6 +86,17 @@ class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
end
end
+class DeveloperWithSelect < ActiveRecord::Base
+ self.table_name = 'developers'
+ default_scope select('name')
+end
+
+class DeveloperWithIncludes < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_many :audit_logs, :foreign_key => :developer_id
+ default_scope includes(:audit_logs)
+end
+
class DeveloperOrderedBySalary < ActiveRecord::Base
self.table_name = 'developers'
default_scope :order => 'salary DESC'
@@ -127,6 +138,21 @@ class ClassMethodDeveloperCalledDavid < ActiveRecord::Base
end
end
+class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base
+ self.table_name = 'developers'
+ scope :david, where(:name => 'David')
+
+ def self.default_scope
+ david
+ end
+end
+
+class LazyBlockReferencingScopeDeveloperCalledDavid < ActiveRecord::Base
+ self.table_name = 'developers'
+ scope :david, where(:name => 'David')
+ default_scope { david }
+end
+
class DeveloperCalledJamis < ActiveRecord::Base
self.table_name = 'developers'
@@ -165,4 +191,48 @@ class ModuleIncludedPoorDeveloperCalledJamis < DeveloperCalledJamis
include SalaryDefaultScope
end
+class EagerDeveloperWithDefaultScope < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id'
+
+ default_scope includes(:projects)
+end
+
+class EagerDeveloperWithClassMethodDefaultScope < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id'
+ def self.default_scope
+ includes(:projects)
+ end
+end
+
+class EagerDeveloperWithLambdaDefaultScope < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id'
+
+ default_scope lambda { includes(:projects) }
+end
+
+class EagerDeveloperWithBlockDefaultScope < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id'
+
+ default_scope { includes(:projects) }
+end
+
+class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id'
+
+ default_scope OpenStruct.new(:call => includes(:projects))
+end
+
+class ThreadsafeDeveloper < ActiveRecord::Base
+ self.table_name = 'developers'
+
+ def self.default_scope
+ sleep 0.05 if Thread.current[:long_default_scope]
+ limit(1)
+ end
+end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index 991e0e051f..11a0f4ff63 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -1,8 +1,10 @@
class Member < ActiveRecord::Base
has_one :current_membership
+ has_one :selected_membership
has_one :membership
has_many :fellow_members, :through => :club, :source => :members
has_one :club, :through => :current_membership
+ has_one :selected_club, :through => :selected_membership, :source => :club
has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club
has_one :hairy_club, :through => :membership, :conditions => {:clubs => {:name => "Moustache and Eyebrow Fancier Club"}}, :source => :club
has_one :sponsor, :as => :sponsorable
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index 905f948c37..bcbb7e42c5 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -7,3 +7,9 @@ class CurrentMembership < Membership
belongs_to :member
belongs_to :club
end
+
+class SelectedMembership < Membership
+ def self.default_scope
+ select("'1' as foo")
+ end
+end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index a58c9bf572..967a3625aa 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -59,8 +59,9 @@ class LoosePerson < ActiveRecord::Base
has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id
-
has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
+
+ accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
end
class LooseDescendant < LoosePerson; end
@@ -70,11 +71,14 @@ class TightPerson < ActiveRecord::Base
attr_accessible :first_name, :gender
attr_accessible :first_name, :gender, :comments, :as => :admin
+ attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes
+ attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes, :as => :admin
has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id
belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id
-
has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id
+
+ accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
end
class TightDescendant < TightPerson; end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 80296032bb..198a963cbc 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -36,6 +36,10 @@ class Post < ActiveRecord::Base
def find_most_recent
find(:first, :order => "id DESC")
end
+
+ def newest
+ created.last
+ end
end
has_many :author_favorites, :through => :author
@@ -162,3 +166,19 @@ class FirstPost < ActiveRecord::Base
has_many :comments, :foreign_key => :post_id
has_one :comment, :foreign_key => :post_id
end
+
+class PostWithDefaultInclude < ActiveRecord::Base
+ self.table_name = 'posts'
+ default_scope includes(:comments)
+ has_many :comments, :foreign_key => :post_id
+end
+
+class PostWithDefaultScope < ActiveRecord::Base
+ self.table_name = 'posts'
+ default_scope :order => :title
+end
+
+class SpecialPostWithDefaultScope < ActiveRecord::Base
+ self.table_name = 'posts'
+ default_scope where(:id => [1, 5,6])
+end \ No newline at end of file
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 6440dbe8ab..fe424e61b2 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -78,11 +78,12 @@ class Topic < ActiveRecord::Base
after_initialize :set_email_address
+ def approved=(val)
+ @custom_approved = val
+ write_attribute(:approved, val)
+ end
+
protected
- def approved=(val)
- @custom_approved = val
- write_attribute(:approved, val)
- end
def default_written_on
self.written_on = Time.now unless attribute_present?("written_on")
diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb
index 79a88db0da..6c45e99671 100644
--- a/activerecord/test/models/toy.rb
+++ b/activerecord/test/models/toy.rb
@@ -1,4 +1,6 @@
class Toy < ActiveRecord::Base
set_primary_key :toy_id
belongs_to :pet
+
+ scope :with_pet, joins(:pet)
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index c8a98f121d..9d5ad16a3c 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -40,9 +40,14 @@ ActiveRecord::Schema.define do
t.references :account
end
+ create_table :aircraft, :force => true do |t|
+ t.string :name
+ end
+
create_table :audit_logs, :force => true do |t|
t.column :message, :string, :null=>false
t.column :developer_id, :integer, :null=>false
+ t.integer :unvalidated_developer_id
end
create_table :authors, :force => true do |t|
@@ -90,6 +95,7 @@ ActiveRecord::Schema.define do
t.integer :car_id
t.string :name
t.boolean :frickinawesome
+ t.string :color
end
create_table "CamelCase", :force => true do |t|
@@ -150,6 +156,8 @@ ActiveRecord::Schema.define do
end
t.string :type
t.integer :taggings_count, :default => 0
+ t.integer :children_count, :default => 0
+ t.integer :parent_id
end
create_table :companies, :force => true do |t|
@@ -300,6 +308,8 @@ ActiveRecord::Schema.define do
t.references :student
end
+ create_table :lint_models, :force => true
+
create_table :line_items, :force => true do |t|
t.integer :invoice_id
t.integer :amount
@@ -453,6 +463,7 @@ ActiveRecord::Schema.define do
create_table :pirates, :force => true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
+ t.integer :non_validated_parrot_id
t.column :created_on, :datetime
t.column :updated_on, :datetime
end
@@ -521,6 +532,7 @@ ActiveRecord::Schema.define do
create_table :ships, :force => true do |t|
t.string :name
t.integer :pirate_id
+ t.integer :update_only_pirate_id
t.datetime :created_at
t.datetime :created_on
t.datetime :updated_at
@@ -655,7 +667,9 @@ ActiveRecord::Schema.define do
t.string :description
t.integer :man_id
t.integer :polymorphic_man_id
- t.string :polymorphic_man_type
+ t.string :polymorphic_man_type
+ t.integer :horrible_polymorphic_man_id
+ t.string :horrible_polymorphic_man_type
end
create_table :interests, :force => true do |t|
@@ -714,6 +728,8 @@ ActiveRecord::Schema.define do
end
execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
+
+ execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
end
end
diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb
new file mode 100644
index 0000000000..6d123688a3
--- /dev/null
+++ b/activerecord/test/support/config.rb
@@ -0,0 +1,43 @@
+require 'yaml'
+require 'erubis'
+require 'fileutils'
+require 'pathname'
+
+module ARTest
+ class << self
+ def config
+ @config ||= read_config
+ end
+
+ private
+
+ def config_file
+ Pathname.new(ENV['ARCONFIG'] || TEST_ROOT + '/config.yml')
+ end
+
+ def read_config
+ unless config_file.exist?
+ FileUtils.cp TEST_ROOT + '/config.example.yml', config_file
+ end
+
+ erb = Erubis::Eruby.new(config_file.read)
+ expand_config(YAML.parse(erb.result(binding)).transform)
+ end
+
+ def expand_config(config)
+ config['connections'].each do |adapter, connection|
+ dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']]
+ dbs.each do |name, dbname|
+ unless connection[name].is_a?(Hash)
+ connection[name] = { 'database' => connection[name] }
+ end
+
+ connection[name]['database'] ||= dbname
+ connection[name]['adapter'] ||= adapter
+ end
+ end
+
+ config
+ end
+ end
+end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
new file mode 100644
index 0000000000..a39794fa39
--- /dev/null
+++ b/activerecord/test/support/connection.rb
@@ -0,0 +1,20 @@
+require 'logger'
+require_dependency 'models/course'
+
+module ARTest
+ def self.connection_name
+ ENV['ARCONN'] || config['default_connection']
+ end
+
+ def self.connection_config
+ config['connections'][connection_name]
+ end
+
+ def self.connect
+ puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}"
+ ActiveRecord::Base.logger = Logger.new("debug.log")
+ ActiveRecord::Base.configurations = connection_config
+ ActiveRecord::Base.establish_connection 'arunit'
+ Course.establish_connection 'arunit2'
+ end
+end