aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md188
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/README.rdoc6
-rw-r--r--activerecord/activerecord.gemspec4
-rw-r--r--activerecord/examples/performance.rb23
-rw-r--r--activerecord/lib/active_record.rb33
-rw-r--r--activerecord/lib/active_record/associations.rb25
-rw-r--r--activerecord/lib/active_record/associations/association.rb5
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb8
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb21
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb14
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb221
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb201
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb88
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb155
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb93
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb27
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb30
-rw-r--r--activerecord/lib/active_record/autosave_association.rb28
-rw-r--r--activerecord/lib/active_record/base.rb1849
-rw-r--r--activerecord/lib/active_record/callbacks.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb277
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb161
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb57
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb57
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb98
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb83
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb92
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb210
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb44
-rw-r--r--activerecord/lib/active_record/connection_handling.rb96
-rw-r--r--activerecord/lib/active_record/core.rb341
-rw-r--r--activerecord/lib/active_record/counter_cache.rb11
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb79
-rw-r--r--activerecord/lib/active_record/explain.rb80
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb21
-rw-r--r--activerecord/lib/active_record/fixtures.rb106
-rw-r--r--activerecord/lib/active_record/fixtures/file.rb5
-rw-r--r--activerecord/lib/active_record/identity_map.rb35
-rw-r--r--activerecord/lib/active_record/inheritance.rb182
-rw-r--r--activerecord/lib/active_record/integration.rb49
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb44
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb22
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb6
-rw-r--r--activerecord/lib/active_record/migration.rb265
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb2
-rw-r--r--activerecord/lib/active_record/model.rb109
-rw-r--r--activerecord/lib/active_record/model_schema.rb307
-rw-r--r--activerecord/lib/active_record/named_scope.rb200
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb4
-rw-r--r--activerecord/lib/active_record/persistence.rb65
-rw-r--r--activerecord/lib/active_record/query_cache.rb45
-rw-r--r--activerecord/lib/active_record/querying.rb58
-rw-r--r--activerecord/lib/active_record/railtie.rb34
-rw-r--r--activerecord/lib/active_record/railties/databases.rake69
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb2
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb26
-rw-r--r--activerecord/lib/active_record/reflection.rb11
-rw-r--r--activerecord/lib/active_record/relation.rb105
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb16
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb49
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb4
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb83
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb57
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb17
-rw-r--r--activerecord/lib/active_record/sanitization.rb194
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb10
-rw-r--r--activerecord/lib/active_record/schema_migration.rb32
-rw-r--r--activerecord/lib/active_record/scoping.rb152
-rw-r--r--activerecord/lib/active_record/scoping/default.rb142
-rw-r--r--activerecord/lib/active_record/scoping/named.rb202
-rw-r--r--activerecord/lib/active_record/serialization.rb6
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb9
-rw-r--r--activerecord/lib/active_record/session_store.rb10
-rw-r--r--activerecord/lib/active_record/test_case.rb24
-rw-r--r--activerecord/lib/active_record/timestamp.rb2
-rw-r--r--activerecord/lib/active_record/transactions.rb14
-rw-r--r--activerecord/lib/active_record/translation.rb22
-rw-r--r--activerecord/lib/active_record/validations/associated.rb5
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb25
-rw-r--r--activerecord/lib/active_record/version.rb4
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb12
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb6
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/migration.rb8
-rw-r--r--activerecord/test/cases/adapter_test.rb245
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb16
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb16
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb89
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb4
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb8
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb6
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb1
-rw-r--r--activerecord/test/cases/associations/eager_test.rb157
-rw-r--r--activerecord/test/cases/associations/extension_test.rb16
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb53
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb39
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb8
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb2
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb4
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations_test.rb23
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb16
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb102
-rw-r--r--activerecord/test/cases/autosave_association_test.rb21
-rw-r--r--activerecord/test/cases/base_test.rb237
-rw-r--r--activerecord/test/cases/binary_test.rb6
-rw-r--r--activerecord/test/cases/calculations_test.rb38
-rw-r--r--activerecord/test/cases/callbacks_test.rb14
-rw-r--r--activerecord/test/cases/column_test.rb29
-rw-r--r--activerecord/test/cases/connection_adapters/abstract_adapter_test.rb54
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/connection_specification_test.rb12
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb23
-rw-r--r--activerecord/test/cases/connection_management_test.rb42
-rw-r--r--activerecord/test/cases/connection_pool_test.rb108
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb41
-rw-r--r--activerecord/test/cases/counter_cache_test.rb20
-rw-r--r--activerecord/test/cases/dup_test.rb8
-rw-r--r--activerecord/test/cases/explain_test.rb101
-rw-r--r--activerecord/test/cases/finder_test.rb11
-rw-r--r--activerecord/test/cases/fixtures_test.rb111
-rw-r--r--activerecord/test/cases/helper.rb20
-rw-r--r--activerecord/test/cases/identity_map/middleware_test.rb3
-rw-r--r--activerecord/test/cases/inclusion_test.rb113
-rw-r--r--activerecord/test/cases/inheritance_test.rb5
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb12
-rw-r--r--activerecord/test/cases/locking_test.rb55
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb3
-rw-r--r--activerecord/test/cases/mass_assignment_security_test.rb63
-rw-r--r--activerecord/test/cases/method_scoping_test.rb12
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb329
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb188
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb60
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb6
-rw-r--r--activerecord/test/cases/migration/helper.rb64
-rw-r--r--activerecord/test/cases/migration/index_test.rb170
-rw-r--r--activerecord/test/cases/migration/logger_test.rb37
-rw-r--r--activerecord/test/cases/migration/rename_column_test.rb195
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb72
-rw-r--r--activerecord/test/cases/migration/table_and_index_test.rb24
-rw-r--r--activerecord/test/cases/migration_test.rb2651
-rw-r--r--activerecord/test/cases/migrator_test.rb377
-rw-r--r--activerecord/test/cases/modules_test.rb2
-rw-r--r--activerecord/test/cases/multiple_db_test.rb1
-rw-r--r--activerecord/test/cases/named_scope_test.rb6
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb26
-rw-r--r--activerecord/test/cases/primary_keys_test.rb48
-rw-r--r--activerecord/test/cases/query_cache_test.rb19
-rw-r--r--activerecord/test/cases/reaper_test.rb81
-rw-r--r--activerecord/test/cases/reflection_test.rb14
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb8
-rw-r--r--activerecord/test/cases/relation_test.rb23
-rw-r--r--activerecord/test/cases/relations_test.rb53
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb20
-rw-r--r--activerecord/test/cases/session_store/session_test.rb10
-rw-r--r--activerecord/test/cases/store_test.rb4
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb3
-rw-r--r--activerecord/test/cases/unconnected_test.rb6
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb41
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb64
-rw-r--r--activerecord/test/cases/validations_test.rb2
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb42
-rw-r--r--activerecord/test/fixtures/admin/randomly_named_a9.yml7
-rw-r--r--activerecord/test/fixtures/admin/randomly_named_b0.yml7
-rw-r--r--activerecord/test/fixtures/dog_lovers.yml4
-rw-r--r--activerecord/test/fixtures/dogs.yml3
-rw-r--r--activerecord/test/fixtures/other_topics.yml42
-rw-r--r--activerecord/test/fixtures/randomly_named_a9.yml7
-rw-r--r--activerecord/test/fixtures/teapots.yml3
-rw-r--r--activerecord/test/migrations/broken/100_migration_that_raises_exception.rb10
-rw-r--r--activerecord/test/migrations/duplicate/3_foo.rb7
-rw-r--r--activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb7
-rw-r--r--activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb7
-rw-r--r--activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb12
-rw-r--r--activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb9
-rw-r--r--activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb12
-rw-r--r--activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb9
-rw-r--r--activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb8
-rw-r--r--activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb12
-rw-r--r--activerecord/test/migrations/rename/1_we_need_things.rb11
-rw-r--r--activerecord/test/migrations/rename/2_rename_things.rb9
-rw-r--r--activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb9
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb (renamed from activerecord/test/migrations/duplicate/1_people_have_last_names.rb)4
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb (renamed from activerecord/test/migrations/duplicate/2_we_need_reminders.rb)0
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb (renamed from activerecord/test/migrations/duplicate/3_innocent_jointable.rb)0
-rw-r--r--activerecord/test/models/admin/randomly_named_c1.rb3
-rw-r--r--activerecord/test/models/author.rb5
-rw-r--r--activerecord/test/models/bird.rb5
-rw-r--r--activerecord/test/models/car.rb2
-rw-r--r--activerecord/test/models/company.rb4
-rw-r--r--activerecord/test/models/country.rb2
-rw-r--r--activerecord/test/models/dashboard.rb4
-rw-r--r--activerecord/test/models/dog.rb4
-rw-r--r--activerecord/test/models/dog_lover.rb4
-rw-r--r--activerecord/test/models/joke.rb4
-rw-r--r--activerecord/test/models/keyboard.rb2
-rw-r--r--activerecord/test/models/legacy_thing.rb2
-rw-r--r--activerecord/test/models/liquid.rb2
-rw-r--r--activerecord/test/models/minivan.rb2
-rw-r--r--activerecord/test/models/mixed_case_monkey.rb2
-rw-r--r--activerecord/test/models/owner.rb2
-rw-r--r--activerecord/test/models/parrot.rb3
-rw-r--r--activerecord/test/models/person.rb7
-rw-r--r--activerecord/test/models/pet.rb2
-rw-r--r--activerecord/test/models/post.rb10
-rw-r--r--activerecord/test/models/randomly_named_c1.rb3
-rw-r--r--activerecord/test/models/speedometer.rb4
-rw-r--r--activerecord/test/models/string_key_object.rb2
-rw-r--r--activerecord/test/models/subscriber.rb2
-rw-r--r--activerecord/test/models/teapot.rb35
-rw-r--r--activerecord/test/models/topic.rb11
-rw-r--r--activerecord/test/models/toy.rb2
-rw-r--r--activerecord/test/models/treaty.rb2
-rw-r--r--activerecord/test/models/warehouse_thing.rb4
-rw-r--r--activerecord/test/schema/schema.rb24
-rw-r--r--activerecord/test/support/connection.rb8
248 files changed, 8866 insertions, 6189 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index c50229e779..a458f9e6e4 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,5 +1,189 @@
+## Rails 4.0.0 (unreleased) ##
+
+* In previous releases, the following would generate a single query with
+ an `OUTER JOIN comments`, rather than two separate queries:
+
+ Post.includes(:comments)
+ .where("comments.name = 'foo'")
+
+ This behaviour relies on matching SQL string, which is an inherently
+ flawed idea unless we write an SQL parser, which we do not wish to
+ do.
+
+ Therefore, it is now deprecated.
+
+ To avoid deprecation warnings and for future compatibility, you must
+ explicitly state which tables you reference, when using SQL snippets:
+
+ Post.includes(:comments)
+ .where("comments.name = 'foo'")
+ .references(:comments)
+
+ Note that you do not need to explicitly specify references in the
+ following cases, as they can be automatically inferred:
+
+ Post.where(comments: { name: 'foo' })
+ Post.where('comments.name' => 'foo')
+ Post.order('comments.name')
+
+ You also do not need to worry about this unless you are doing eager
+ loading. Basically, don't worry unless you see a deprecation warning
+ or (in future releases) an SQL error due to a missing JOIN.
+
+ [Jon Leighton]
+
+* Support for the `schema_info` table has been dropped. Please
+ switch to `schema_migrations`.
+
+* Connections *must* be closed at the end of a thread. If not, your
+ connection pool can fill and an exception will be raised.
+
+* Added the `ActiveRecord::Model` module which can be included in a
+ class as an alternative to inheriting from `ActiveRecord::Base`:
+
+ class Post
+ include ActiveRecord::Model
+ end
+
+ Please note:
+
+ * Up until now it has been safe to assume that all AR models are
+ descendants of `ActiveRecord::Base`. This is no longer a safe
+ assumption, but it may transpire that there are areas of the
+ code which still make this assumption. So there may be
+ 'teething difficulties' with this feature. (But please do try it
+ and report bugs.)
+
+ * Plugins & libraries etc that add methods to `ActiveRecord::Base`
+ will not be compatible with `ActiveRecord::Model`. Those libraries
+ should add to `ActiveRecord::Model` instead (which is included in
+ `Base`), or better still, avoid monkey-patching AR and instead
+ provide a module that users can include where they need it.
+
+ * To minimise the risk of conflicts with other code, it is
+ advisable to include `ActiveRecord::Model` early in your class
+ definition.
+
+ *Jon Leighton*
+
+* PostgreSQL hstore records can be created.
+
+* PostgreSQL hstore types are automatically deserialized from the database.
+
## Rails 3.2.0 (unreleased) ##
+* Added a `with_lock` method to ActiveRecord objects, which starts
+ a transaction, locks the object (pessimistically) and yields to the block.
+ The method takes one (optional) parameter and passes it to `lock!`.
+
+ Before:
+
+ class Order < ActiveRecord::Base
+ def cancel!
+ transaction do
+ lock!
+ # ... cancelling logic
+ end
+ end
+ end
+
+ After:
+
+ class Order < ActiveRecord::Base
+ def cancel!
+ with_lock do
+ # ... cancelling logic
+ end
+ end
+ end
+
+ *Olek Janiszewski*
+
+* 'on' and 'ON' boolean columns values are type casted to true
+ *Santiago Pastorino*
+
+* Added ability to run migrations only for given scope, which allows
+ to run migrations only from one engine (for example to revert changes
+ from engine that you want to remove).
+
+ Example:
+ rake db:migrate SCOPE=blog
+
+ *Piotr Sarnacki*
+
+* Migrations copied from engines are now scoped with engine's name,
+ for example 01_create_posts.blog.rb. *Piotr Sarnacki*
+
+* Implements `AR::Base.silence_auto_explain`. This method allows the user to
+ selectively disable automatic EXPLAINs within a block. *fxn*
+
+* Implements automatic EXPLAIN logging for slow queries.
+
+ A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds`
+ determines what's to be considered a slow query. Setting that to `nil` disables
+ this feature. Defaults are 0.5 in development mode, and `nil` in test and production
+ modes.
+
+ As of this writing there's support for SQLite, MySQL (mysql2 adapter), and
+ PostgreSQL.
+
+ *fxn*
+
+* Implemented ActiveRecord::Relation#pluck method
+
+ Method returns Array of column value from table under ActiveRecord model
+
+ Client.pluck(:id)
+
+ *Bogdan Gusiev*
+
+* Automatic closure of connections in threads is deprecated. For example
+ the following code is deprecated:
+
+ Thread.new { Post.find(1) }.join
+
+ It should be changed to close the database connection at the end of
+ the thread:
+
+ Thread.new {
+ Post.find(1)
+ Post.connection.close
+ }.join
+
+ Only people who spawn threads in their application code need to worry
+ about this change.
+
+* Deprecated:
+
+ * `set_table_name`
+ * `set_inheritance_column`
+ * `set_sequence_name`
+ * `set_primary_key`
+ * `set_locking_column`
+
+ Use an assignment method instead. For example, instead of `set_table_name`, use `self.table_name=`:
+
+ class Project < ActiveRecord::Base
+ self.table_name = "project"
+ end
+
+ Or define your own `self.table_name` method:
+
+ class Post < ActiveRecord::Base
+ def self.table_name
+ "special_" + super
+ end
+ end
+ Post.table_name # => "special_posts"
+
+ *Jon Leighton*
+
+* Generated association methods are created within a separate module to allow overriding and
+ composition using `super`. For a class named `MyModel`, the module is named
+ `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after
+ the `generated_attributes_methods` module defined in ActiveModel, so association methods
+ override attribute methods of the same name. *Josh Susser*
+
* Implemented ActiveRecord::Relation#explain. *fxn*
* Add ActiveRecord::Relation#uniq for generating unique queries.
@@ -65,7 +249,7 @@
*Aaron Christy*
-## Rails 3.1.3 (unreleased) ##
+## Rails 3.1.3 (November 20, 2011) ##
* Perf fix: If we're deleting all records in an association, don't add a IN(..) clause
to the query. *GH 3672*
@@ -78,7 +262,7 @@
*Christos Zisopoulos and Kenny J*
-## Rails 3.1.2 (unreleased) ##
+## Rails 3.1.2 (November 18, 2011) ##
* Fix bug with PostgreSQLAdapter#indexes. When the search path has multiple schemas, spaces
were not being stripped from the schema names after the first.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index c73d1af096..03bde18130 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2011 David Heinemeier Hansson
+Copyright (c) 2004-2012 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index 70922ef864..30a66ff5f0 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -143,7 +143,7 @@ A short rundown of some of the major features:
* 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 = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
@@ -208,7 +208,9 @@ Source code can be downloaded as part of the Rails project on GitHub
== License
-Active Record is released under the MIT license.
+Active Record is released under the MIT license:
+
+* http://www.opensource.org/licenses/MIT
== Support
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 52a3d4a501..8484e1093e 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.summary = 'Object-relational mapper framework (part of Rails).'
s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.'
- s.required_ruby_version = '>= 1.8.7'
+ s.required_ruby_version = '>= 1.9.3'
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
@@ -21,6 +21,6 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
- s.add_dependency('arel', '~> 3.0.0.pre')
+ s.add_dependency('arel', '~> 3.0.0')
s.add_dependency('tzinfo', '~> 0.3.29')
end
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index 63822731d5..31f3e02bb8 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -1,6 +1,6 @@
TIMES = (ENV['N'] || 10000).to_i
-require 'rubygems'
+require File.expand_path('../../../load_paths', __FILE__)
require "active_record"
conn = { :adapter => 'sqlite3', :database => ':memory:' }
@@ -29,6 +29,14 @@ class Exhibit < ActiveRecord::Base
def look; attributes end
def feel; look; user.name end
+ def self.with_name
+ where("name IS NOT NULL")
+ end
+
+ def self.with_notes
+ where("notes IS NOT NULL")
+ end
+
def self.look(exhibits) exhibits.each { |e| e.look } end
def self.feel(exhibits) exhibits.each { |e| e.feel } end
end
@@ -37,7 +45,14 @@ puts 'Generating data...'
module ActiveRecord
class Faker
- LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. Praesent varius tincidunt commodo".split
+ LOREM = %Q{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit.
+ Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem.
+ Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim,
+ tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae,
+ varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum
+ tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero.
+ Praesent varius tincidunt commodo}.split
+
def self.name
LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join ' '
end
@@ -102,6 +117,10 @@ Benchmark.bm(46) do |x|
TIMES.times { Exhibit.first.look }
end
+ x.report 'Model.named_scope' do
+ TIMES.times { Exhibit.limit(10).with_name.with_notes }
+ end
+
x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do
(TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) }
end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 3572c640eb..78e958442f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2011 David Heinemeier Hansson
+# Copyright (c) 2004-2012 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -39,6 +39,7 @@ module ActiveRecord
autoload :Aggregations
autoload :Associations
autoload :AttributeMethods
+ autoload :AttributeAssignment
autoload :AutosaveAssociation
autoload :Relation
@@ -50,31 +51,45 @@ module ActiveRecord
autoload :PredicateBuilder
autoload :SpawnMethods
autoload :Batches
+ autoload :Explain
+ autoload :Delegation
end
autoload :Base
autoload :Callbacks
+ autoload :Core
autoload :CounterCache
+ autoload :ConnectionHandling
+ autoload :DynamicMatchers
autoload :DynamicFinderMatch
autoload :DynamicScopeMatch
+ autoload :Explain
+ autoload :IdentityMap
+ autoload :Inheritance
+ autoload :Integration
autoload :Migration
autoload :Migrator, 'active_record/migration'
- autoload :NamedScope
+ autoload :Model
+ autoload :ModelSchema
autoload :NestedAttributes
autoload :Observer
autoload :Persistence
autoload :QueryCache
+ autoload :Querying
+ autoload :ReadonlyAttributes
autoload :Reflection
autoload :Result
+ autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
+ autoload :Scoping
autoload :Serialization
- autoload :Store
autoload :SessionStore
+ autoload :Store
autoload :Timestamp
autoload :Transactions
+ autoload :Translation
autoload :Validations
- autoload :IdentityMap
end
module Coders
@@ -92,6 +107,7 @@ module ActiveRecord
autoload :Read
autoload :TimeZoneConversion
autoload :Write
+ autoload :Serialization
end
end
@@ -113,6 +129,15 @@ module ActiveRecord
end
end
+ module Scoping
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Named
+ autoload :Default
+ end
+ end
+
autoload :TestCase
autoload :TestFixtures, 'active_record/fixtures'
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 34684ad2f5..58725246c8 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
@@ -70,7 +69,7 @@ module ActiveRecord
end
end
- class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
+ class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
end
@@ -196,6 +195,26 @@ module ActiveRecord
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
#
+ # === Overriding generated methods
+ #
+ # Association methods are generated in a module that is included into the model class,
+ # which allows you to easily override with your own methods and call the original
+ # generated method with +super+. For example:
+ #
+ # class Car < ActiveRecord::Base
+ # belongs_to :owner
+ # belongs_to :old_owner
+ # def owner=(new_owner)
+ # self.old_owner = self.owner
+ # super
+ # end
+ # end
+ #
+ # If your model class is <tt>Project</tt>, the module is
+ # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
+ # included in the model class immediately after the (anonymous) generated attributes methods
+ # module, meaning an association will override the methods for an attribute with the same name.
+ #
# === A word of warning
#
# Don't create associations that have the same name as instance methods of
@@ -1165,7 +1184,7 @@ module ActiveRecord
# has_many :subscribers, :through => :subscriptions, :source => :user
# has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new {
# %Q{
- # SELECT DISTINCT people.*
+ # SELECT DISTINCT *
# FROM people p, post_subscriptions ps
# WHERE ps.post_id = #{id} AND ps.person_id = p.id
# ORDER BY p.first_name
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index d1e3ff8e38..7887d59aad 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -231,10 +231,7 @@ module ActiveRecord
def build_record(attributes, options)
reflection.build_association(attributes, options) do |record|
- record.assign_attributes(
- create_scope.except(*record.changed),
- :without_protection => true
- )
+ record.assign_attributes(create_scope.except(*record.changed), :without_protection => true)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 6f8b76abda..0209ce36df 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -15,12 +15,12 @@ module ActiveRecord
def scope
scope = klass.unscoped
- scope = scope.extending(*Array.wrap(options[:extend]))
+ scope = scope.extending(*Array(options[:extend]))
# It's okay to just apply all these like this. The options will only be present if the
# association supports that option; this is enforced by the association builder.
scope = scope.apply_finder_options(options.slice(
- :readonly, :include, :order, :limit, :joins, :group, :having, :offset, :select))
+ :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select))
if options[:through] && !options[:include]
scope = scope.includes(source_options[:include])
@@ -90,8 +90,11 @@ module ActiveRecord
scope = scope.joins(join(foreign_table, constraint))
- unless conditions.empty?
- scope = scope.where(sanitize(conditions, table))
+ conditions.each do |condition|
+ condition = interpolate(condition)
+ condition = { (table.table_alias || table.name) => condition } unless i == 0
+
+ scope = scope.where(condition)
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 96fca97440..6e2e5f9de0 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,7 +1,7 @@
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class_attribute :valid_options
- self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate]
+ self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references]
# Set by subclasses
class_attribute :macro
@@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder
@model, @name, @options = model, name, options
end
+ def mixin
+ @model.generated_feature_methods
+ end
+
def build
validate_options
reflection = model.create_reflection(self.class.macro, name, options, model)
@@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder
def define_readers
name = self.name
-
- model.redefine_method(name) do |*params|
+ mixin.redefine_method(name) do |*params|
association(name).reader(*params)
end
end
def define_writers
name = self.name
-
- model.redefine_method("#{name}=") do |value|
+ mixin.redefine_method("#{name}=") do |value|
association(name).writer(value)
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index f6d26840c2..4183c222de 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -25,16 +25,18 @@ module ActiveRecord::Associations::Builder
name = self.name
method_name = "belongs_to_counter_cache_after_create_for_#{name}"
- model.redefine_method(method_name) do
+ mixin.redefine_method(method_name) do
record = send(name)
record.class.increment_counter(cache_column, record.id) unless record.nil?
end
model.after_create(method_name)
method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
- model.redefine_method(method_name) do
- record = send(name)
- record.class.decrement_counter(cache_column, record.id) unless record.nil?
+ mixin.redefine_method(method_name) do
+ unless marked_for_destruction?
+ record = send(name)
+ record.class.decrement_counter(cache_column, record.id) unless record.nil?
+ end
end
model.before_destroy(method_name)
@@ -48,7 +50,7 @@ module ActiveRecord::Associations::Builder
method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
touch = options[:touch]
- model.redefine_method(method_name) do
+ mixin.redefine_method(method_name) do
record = send(name)
unless record.nil?
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index f62209a226..768f70b6c9 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -32,7 +32,7 @@ module ActiveRecord::Associations::Builder
private
def wrap_block_extension
- options[:extend] = Array.wrap(options[:extend])
+ options[:extend] = Array(options[:extend])
if block_extension
silence_warnings do
@@ -51,14 +51,14 @@ module ActiveRecord::Associations::Builder
# TODO : why do i need method_defined? I think its because of the inheritance chain
model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym]))
+ model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
end
def define_readers
super
name = self.name
- model.redefine_method("#{name.to_s.singularize}_ids") do
+ mixin.redefine_method("#{name.to_s.singularize}_ids") do
association(name).ids_reader
end
end
@@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder
super
name = self.name
- model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
+ mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
association(name).ids_writer(ids)
end
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 30fc44b4c2..0b634ab944 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
@@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder
model.send(:include, Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def destroy_associations
- association(#{name.to_sym.inspect}).delete_all
+ association(#{name.to_sym.inspect}).delete_all_on_destroy
super
end
RUBY
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index ecbc70888f..fc6799fb15 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -28,15 +28,10 @@ module ActiveRecord::Associations::Builder
def define_destroy_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
send(name).each do |o|
# No point in executing the counter update since we're going to destroy the parent anyway
- counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
- if o.respond_to?(counter_method)
- class << o
- self
- end.send(:define_method, counter_method, Proc.new {})
- end
+ o.mark_for_destruction
end
send(name).delete_all
@@ -45,15 +40,21 @@ module ActiveRecord::Associations::Builder
def define_delete_all_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
+ association(name).delete_all_on_destroy
+ end
+ end
+
+ def define_nullify_dependency_method
+ name = self.name
+ mixin.redefine_method(dependency_method_name) do
send(name).delete_all
end
end
- alias :define_nullify_dependency_method :define_delete_all_dependency_method
def define_restrict_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 88c0d3e90f..7a6cd3890f 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder
end
def define_destroy_dependency_method
- model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
- def #{dependency_method_name}
- association(#{name.to_sym.inspect}).delete
- end
- eoruby
+ name = self.name
+ mixin.redefine_method(dependency_method_name) do
+ association(name).delete
+ end
end
alias :define_delete_dependency_method :define_destroy_dependency_method
alias :define_nullify_dependency_method :define_destroy_dependency_method
def define_restrict_dependency_method
name = self.name
- model.redefine_method(dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 0cbbba041a..436b6c1524 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder
def define_constructors
name = self.name
- model.redefine_method("build_#{name}") do |*params, &block|
+ mixin.redefine_method("build_#{name}") do |*params, &block|
association(name).build(*params, &block)
end
- model.redefine_method("create_#{name}") do |*params, &block|
+ mixin.redefine_method("create_#{name}") do |*params, &block|
association(name).create(*params, &block)
end
- model.redefine_method("create_#{name}!") do |*params, &block|
+ mixin.redefine_method("create_#{name}!") do |*params, &block|
association(name).create!(*params, &block)
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 362f1053cd..7aed64d48c 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/array/wrap'
-
module ActiveRecord
module Associations
# = Active Record Association Collection
@@ -49,17 +47,25 @@ module ActiveRecord
end
else
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+ relation = scoped
- scoped.select(column).map! do |record|
- record.send(reflection.association_primary_key)
+ including = (relation.eager_load_values + relation.includes_values).uniq
+
+ if including.any?
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
+ relation = join_dependency.join_associations.inject(relation) do |r, association|
+ association.join_relation(r)
+ end
end
+
+ relation.uniq.pluck(column)
end
end
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
pk_column = reflection.primary_key_column
- ids = Array.wrap(ids).reject { |id| id.blank? }
+ ids = Array(ids).reject { |id| id.blank? }
ids.map! { |i| pk_column.type_cast(i) }
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
end
@@ -152,6 +158,13 @@ module ActiveRecord
end
end
+ # Called when the association is declared as :dependent => :delete_all. This is
+ # an optimised version which avoids loading the records into memory. Not really
+ # for public consumption.
+ def delete_all_on_destroy
+ scoped.delete_all
+ end
+
# Destroy all the records from this association.
#
# See destroy for more info.
@@ -235,7 +248,7 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if owner.new_record? || (loaded? && !options[:uniq])
+ if !find_target? || (loaded? && !options[:uniq])
target.size
elsif !loaded? && options[:group]
load_target.size
@@ -385,12 +398,7 @@ module ActiveRecord
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)
+ if mem_record = memory.delete(record)
(record.attribute_names - mem_record.changes.keys).each do |name|
mem_record[name] = record[name]
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 3181ca9a32..ba01df00e3 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -39,10 +39,9 @@ module ActiveRecord
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
- :lock, :readonly, :having, :to => :scoped
+ :lock, :readonly, :having, :pluck, :to => :scoped
- delegate :target, :load_target, :loaded?, :scoped,
- :to => :@association
+ delegate :target, :load_target, :loaded?, :to => :@association
delegate :select, :find, :first, :last,
:build, :create, :create!,
@@ -53,7 +52,7 @@ module ActiveRecord
def initialize(association)
@association = association
- Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
+ Array(association.options[:extend]).each { |ext| proxy_extend(ext) }
end
alias_method :new, :build
@@ -62,6 +61,13 @@ module ActiveRecord
@association
end
+ def scoped
+ association = @association
+ association.scoped.extending do
+ define_method(:proxy_association) { association }
+ end
+ end
+
def respond_to?(name, include_private = false)
super ||
(load_target && target.respond_to?(name, include_private)) ||
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 1f917f58f2..a4cea99372 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
@@ -32,6 +32,10 @@ module ActiveRecord
record
end
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
+ # SQL-only implementation.
+ alias delete_all_on_destroy delete_all
+
private
def count_records
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 3353cdf1ef..059e6c77bc 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -89,12 +89,8 @@ module ActiveRecord
records.each { |r| r.destroy }
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- scope = scoped
-
- unless records == load_target
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = scoped.where(reflection.association_primary_key => keys)
- end
+ keys = records.map { |r| r[reflection.association_primary_key] }
+ scope = scoped.where(reflection.association_primary_key => keys)
if method == :delete_all
update_counter(-scope.delete_all)
@@ -103,6 +99,10 @@ module ActiveRecord
end
end
end
+
+ def foreign_key_present?
+ owner.attribute_present?(reflection.association_primary_key)
+ end
end
end
end
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 7e6e3be382..9657cb081d 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -54,6 +54,10 @@ module ActiveRecord
record
end
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
+ # SQL-only implementation.
+ alias delete_all_on_destroy delete_all
+
private
def through_association
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 6c878f0f00..827b01c5ac 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -184,7 +184,7 @@ module ActiveRecord
macro = join_part.reflection.macro
if macro == :has_one
- return if record.association_cache.key?(join_part.reflection.name)
+ return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
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 03963ab060..0d7d28e458 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -95,8 +95,11 @@ module ActiveRecord
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))
+ conditions.each do |condition|
+ condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
+ condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
+
+ constraint = constraint.and(condition)
end
relation.from(join(table, constraint))
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index f83138195c..cea6ad6944 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -40,16 +40,6 @@ module ActiveRecord
def join(table, constraint)
table.create_join(table, table.create_on(constraint), join_type)
end
-
- def sanitize(conditions, table)
- conditions = conditions.map do |condition|
- condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
- condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
- condition
- end
-
- conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 779f8164cc..253998fb23 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -95,8 +95,8 @@ module ActiveRecord
def build_scope
scope = klass.scoped
- scope = scope.where(process_conditions(options[:conditions]))
- scope = scope.where(process_conditions(preload_options[:conditions]))
+ scope = scope.where(interpolate(options[:conditions]))
+ scope = scope.where(interpolate(preload_options[:conditions]))
scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
scope = scope.includes(preload_options[:include] || options[:include])
@@ -112,13 +112,11 @@ module ActiveRecord
scope
end
- def process_conditions(conditions)
+ def interpolate(conditions)
if conditions.respond_to?(:to_proc)
- conditions = klass.send(:instance_eval, &conditions)
- end
-
- if conditions
- klass.send(:sanitize_sql, conditions)
+ klass.send(:instance_eval, &conditions)
+ else
+ conditions
end
end
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
new file mode 100644
index 0000000000..bf9fe70b31
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -0,0 +1,221 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module AttributeAssignment
+ extend ActiveSupport::Concern
+ include ActiveModel::MassAssignmentSecurity
+
+ module ClassMethods
+ private
+
+ # The primary key and inheritance column can never be set by mass-assignment for security reasons.
+ def attributes_protected_by_default
+ default = [ primary_key, inheritance_column ]
+ default << 'id' unless primary_key.eql? 'id'
+ default
+ end
+ end
+
+ # Allows you to set all the attributes at once by passing in a hash with keys
+ # matching the attribute names (which again matches the column names).
+ #
+ # If any attributes are protected by either +attr_protected+ or
+ # +attr_accessible+ then only settable attributes will be assigned.
+ #
+ # class User < ActiveRecord::Base
+ # attr_protected :is_admin
+ # end
+ #
+ # user = User.new
+ # user.attributes = { :username => 'Phusion', :is_admin => true }
+ # user.username # => "Phusion"
+ # user.is_admin? # => false
+ def attributes=(new_attributes)
+ return unless new_attributes.is_a?(Hash)
+
+ assign_attributes(new_attributes)
+ end
+
+ # Allows you to set all the attributes for a particular mass-assignment
+ # security role by passing in a hash of attributes with keys matching
+ # the attribute names (which again matches the column names) and the role
+ # name using the :as option.
+ #
+ # To bypass mass-assignment security you can use the :without_protection => true
+ # option.
+ #
+ # class User < ActiveRecord::Base
+ # attr_accessible :name
+ # attr_accessible :name, :is_admin, :as => :admin
+ # end
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true })
+ # user.name # => "Josh"
+ # user.is_admin? # => false
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
+ # user.name # => "Josh"
+ # user.is_admin? # => true
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
+ # user.name # => "Josh"
+ # user.is_admin? # => true
+ def assign_attributes(new_attributes, options = {})
+ return unless new_attributes
+
+ attributes = new_attributes.stringify_keys
+ multi_parameter_attributes = []
+ nested_parameter_attributes = []
+ @mass_assignment_options = options
+
+ unless options[:without_protection]
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
+ end
+
+ attributes.each do |k, v|
+ if k.include?("(")
+ multi_parameter_attributes << [ k, v ]
+ elsif respond_to?("#{k}=")
+ if v.is_a?(Hash)
+ nested_parameter_attributes << [ k, v ]
+ else
+ send("#{k}=", v)
+ end
+ else
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
+ end
+ end
+
+ # assign any deferred nested attributes after the base attributes have been set
+ nested_parameter_attributes.each do |k,v|
+ send("#{k}=", v)
+ end
+
+ @mass_assignment_options = nil
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ end
+
+ protected
+
+ def mass_assignment_options
+ @mass_assignment_options ||= {}
+ end
+
+ def mass_assignment_role
+ mass_assignment_options[:as] || :default
+ end
+
+ private
+
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
+ # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
+ # attribute will be set to nil.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
+
+ def instantiate_time_object(name, values)
+ if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
+ Time.zone.local(*values)
+ else
+ Time.time_with_datetime_fallback(self.class.default_timezone, *values)
+ end
+ end
+
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ errors = []
+ callstack.each do |name, values_with_empty_parameters|
+ begin
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
+ rescue => ex
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
+ end
+ end
+ unless errors.empty?
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
+ end
+ end
+
+ def read_value_from_parameter(name, values_hash_from_param)
+ klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
+ if values_hash_from_param.values.all?{|v|v.nil?}
+ nil
+ elsif klass == Time
+ read_time_parameter_value(name, values_hash_from_param)
+ elsif klass == Date
+ read_date_parameter_value(name, values_hash_from_param)
+ else
+ read_other_parameter_value(klass, name, values_hash_from_param)
+ end
+ end
+
+ def read_time_parameter_value(name, values_hash_from_param)
+ # 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 Time bits are not there, then default to 0
+ (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)
+ 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 # 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
+
+ def read_other_parameter_value(klass, name, values_hash_from_param)
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
+ values = (1..max_position).collect do |position|
+ raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
+ values_hash_from_param[position]
+ end
+ klass.new(*values)
+ end
+
+ def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
+ [values_hash_from_param.keys.max,upper_cap].min
+ end
+
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = { }
+
+ pairs.each do |pair|
+ multiparameter_name, value = pair
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
+
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
+ end
+
+ attributes
+ end
+
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
+ end
+
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index d7bfaa5655..a1e34a3aa1 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -7,35 +7,59 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
+ included do
+ include Read
+ include Write
+ include BeforeTypeCast
+ include Query
+ include PrimaryKey
+ include TimeZoneConversion
+ include Dirty
+ include Serialization
+
+ # 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)).
+ # (Alias for the protected read_attribute method).
+ def [](attr_name)
+ read_attribute(attr_name)
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ def []=(attr_name, value)
+ write_attribute(attr_name, value)
+ end
+ end
+
module ClassMethods
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
- return if attribute_methods_generated?
-
- if base_class == self
+ # Use a mutex; we don't want two thread simaltaneously trying to define
+ # attribute methods.
+ @attribute_methods_mutex.synchronize do
+ return if attribute_methods_generated?
+ superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
- else
- base_class.define_attribute_methods
end
end
def attribute_methods_generated?
- if base_class == self
- @attribute_methods_generated ||= false
- else
- base_class.attribute_methods_generated?
- end
+ @attribute_methods_generated ||= false
end
- def undefine_attribute_methods(*args)
- if base_class == self
- super
- @attribute_methods_generated = false
- else
- base_class.undefine_attribute_methods(*args)
- end
+ # We will define the methods as instance methods, but will call them as singleton
+ # methods. This allows us to use method_defined? to check if the method exists,
+ # which is fast and won't give any false positives from the ancestors (because
+ # there are no ancestors).
+ def generated_external_attribute_methods
+ @generated_external_attribute_methods ||= Module.new { extend self }
+ end
+
+ def undefine_attribute_methods
+ super if attribute_methods_generated?
+ @attribute_methods_generated = false
end
def instance_method_already_implemented?(method_name)
@@ -43,19 +67,46 @@ module ActiveRecord
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
end
- super
+ if [Base, Model].include?(active_record_super)
+ super
+ else
+ # If B < A and A defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
+ defined && !ActiveRecord::Base.method_defined?(method_name) || super
+ end
end
# 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
- superclass = ActiveRecord::Base.superclass
+ def dangerous_attribute_method?(name)
+ method_defined_within?(name, Base)
+ end
+
+ def method_defined_within?(name, klass, sup = klass.superclass)
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
+ if sup.method_defined?(name) || sup.private_method_defined?(name)
+ klass.instance_method(name).owner != sup.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
- (active_record.method_defined?(method_name) ||
- active_record.private_method_defined?(method_name)) &&
- !superclass.method_defined?(method_name) &&
- !superclass.private_method_defined?(method_name)
+ def attribute_method?(attribute)
+ 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
end
@@ -94,9 +145,105 @@ module ActiveRecord
super
end
+ # Returns true if the given attribute is in the attributes hash
+ def has_attribute?(attr_name)
+ @attributes.has_key?(attr_name.to_s)
+ end
+
+ # Returns an array of names for the attributes available on this object.
+ def attribute_names
+ @attributes.keys
+ end
+
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ def attributes
+ Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
+ end
+
+ # Returns an <tt>#inspect</tt>-like string for the value of the
+ # 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.
+ #
+ # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
+ #
+ # person.attribute_for_inspect(:name)
+ # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
+ #
+ # person.attribute_for_inspect(:created_at)
+ # # => '"2009-01-12 04:48:57"'
+ def attribute_for_inspect(attr_name)
+ value = read_attribute(attr_name)
+
+ if value.is_a?(String) && value.length > 50
+ "#{value[0..50]}...".inspect
+ elsif value.is_a?(Date) || value.is_a?(Time)
+ %("#{value.to_s(:db)}")
+ else
+ value.inspect
+ end
+ end
+
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
+ def attribute_present?(attribute)
+ value = read_attribute(attribute)
+ !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
+ end
+
+ # Returns the column object for the named attribute.
+ def column_for_attribute(name)
+ self.class.columns_hash[name.to_s]
+ end
+
protected
- def attribute_method?(attr_name)
- attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
+
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
+ attribute_names.each do |name|
+ attributes[name] = clone_attribute_value(reader_method, name)
end
+ attributes
+ end
+
+ def clone_attribute_value(reader_method, attribute_name)
+ value = send(reader_method, attribute_name)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
+ end
+
+ # Returns a copy of the attributes hash where all the values have been safely quoted for use in
+ # an Arel insert/update method.
+ def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
+ attrs = {}
+ klass = self.class
+ arel_table = klass.arel_table
+
+ attribute_names.each do |name|
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
+
+ if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
+
+ value = if klass.serialized_attributes.include?(name)
+ @attributes[name].serialized_value
+ else
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already type
+ # casted, this code could be simplified
+ read_attribute(name)
+ end
+
+ attrs[arel_table[name]] = value
+ end
+ end
+ end
+
+ attrs
+ end
+
+ def attribute_method?(attr_name)
+ attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 3eff3d54e3..40e4a97e73 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -13,7 +13,7 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- class_attribute :partial_updates
+ config_attribute :partial_updates
self.partial_updates = true
end
@@ -34,9 +34,9 @@ module ActiveRecord
@previously_changed = changes
@changed_attributes.clear
end
- rescue
- IdentityMap.remove(self) if IdentityMap.enabled?
- raise
+ rescue
+ IdentityMap.remove(self) if IdentityMap.enabled?
+ raise
end
# <tt>reload</tt> the record and clears changed attributes.
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index a404a5edd7..7c59664703 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -5,15 +5,50 @@ module ActiveRecord
# 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 = self.id
[key] if key
end
+ # Returns the primary key value
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary key value
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Queries the primary key value
+ def id?
+ query_attribute(self.class.primary_key)
+ end
+
module ClassMethods
+ def define_method_attribute(attr_name)
+ super
+
+ if attr_name == primary_key && attr_name != 'id'
+ generated_attribute_methods.send(:alias_method, :id, primary_key)
+ generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__
+ def id(v, attributes, attributes_cache, attr_name)
+ attr_name = '#{primary_key}'
+ send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name)
+ end
+ CODE
+ end
+ end
+
+ def dangerous_attribute_method?(method_name)
+ super && !['id', 'id=', 'id?'].include?(method_name)
+ end
+
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
- # primary_key_prefix_type setting, though.
+ # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment,
+ # remember to let your database generate them or include the key in +attr_accessible+.
def primary_key
- @primary_key ||= reset_primary_key
+ @primary_key = reset_primary_key unless defined? @primary_key
+ @primary_key
end
# Returns a quoted version of the primary key name, used to construct SQL statements.
@@ -22,11 +57,11 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- key = self == base_class ? get_primary_key(base_class.name) :
- base_class.primary_key
-
- set_primary_key(key)
- key
+ if self == base_class
+ self.primary_key = get_primary_key(base_class.name)
+ else
+ self.primary_key = base_class.primary_key
+ end
end
def get_primary_key(base_name) #:nodoc:
@@ -38,35 +73,32 @@ module ActiveRecord
when :table_name_with_underscore
base_name.foreign_key
else
- if ActiveRecord::Base != self && connection.table_exists?(table_name)
- connection.primary_key(table_name)
+ if ActiveRecord::Base != self && table_exists?
+ connection.schema_cache.primary_keys[table_name]
else
'id'
end
end
end
- attr_accessor :original_primary_key
-
- # Attribute writer for the primary key column
- def primary_key=(value)
- @quoted_primary_key = nil
- @primary_key = value
- end
-
- # Sets the name of the primary key column to use to the given value,
- # or (if the value is nil or false) to the value returned by the given
- # block.
+ # Sets the name of the primary key column.
+ #
+ # class Project < ActiveRecord::Base
+ # self.primary_key = "sysid"
+ # end
+ #
+ # You can also define the primary_key method yourself:
#
# class Project < ActiveRecord::Base
- # set_primary_key "sysid"
+ # def self.primary_key
+ # "foo_" + super
+ # end
# end
- def set_primary_key(value = nil, &block)
- @quoted_primary_key = nil
- @primary_key ||= ''
- self.original_primary_key = @primary_key
- value &&= value.to_s
- self.primary_key = block_given? ? instance_eval(&block) : value
+ # Project.primary_key # => "foo_id"
+ def primary_key=(value)
+ @original_primary_key = @primary_key if defined?(@primary_key)
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 4a5afcd585..964c4123ef 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -6,11 +6,8 @@ module ActiveRecord
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
included do
- cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
+ config_attribute :attribute_types_cached_by_default, :global => true
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
- # Undefine id so it can be used as an attribute name
- undef_method(:id) if method_defined?(:id)
end
module ClassMethods
@@ -32,107 +29,101 @@ module ActiveRecord
cached_attributes.include?(attr_name)
end
+ def undefine_attribute_methods
+ generated_external_attribute_methods.module_eval do
+ instance_methods.each { |m| undef_method(m) }
+ end
+
+ super
+ end
+
+ def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc:
+ return unless attr_name
+ attr_name = attr_name.to_s
+
+ if generated_external_attribute_methods.method_defined?(attr_name)
+ if attributes.has_key?(attr_name) || attr_name == 'id'
+ generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name)
+ end
+ elsif !attribute_methods_generated?
+ # If we haven't generated the caster methods yet, do that and
+ # then try again
+ define_attribute_methods
+ type_cast_attribute(attr_name, attributes, cache)
+ else
+ # If we get here, the attribute has no associated DB column, so
+ # just return it verbatim.
+ attributes[attr_name]
+ end
+ end
+
protected
+ # We want to generate the methods via module_eval rather than define_method,
+ # because define_method is slower on dispatch and uses more memory (because it
+ # creates a closure).
+ #
+ # But sometimes the database might return columns with characters that are not
+ # allowed in normal method names (like 'my_column(omg)'. So to work around this
+ # we first define with the __temp__ identifier, and then use alias method to
+ # rename it to what we want.
def define_method_attribute(attr_name)
- if serialized_attributes.include?(attr_name)
- define_read_method_for_serialized_attribute(attr_name)
- else
- define_read_method(attr_name, attr_name, columns_hash[attr_name])
- end
+ cast_code = attribute_cast_code(attr_name)
- if attr_name == primary_key && attr_name != "id"
- define_read_method('id', attr_name, columns_hash[attr_name])
- end
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__
+ #{internal_attribute_access_code(attr_name, cast_code)}
+ end
+ alias_method '#{attr_name}', :__temp__
+ undef_method :__temp__
+ STR
+
+ generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__(v, attributes, attributes_cache, attr_name)
+ #{external_attribute_access_code(attr_name, cast_code)}
+ end
+ alias_method '#{attr_name}', :__temp__
+ undef_method :__temp__
+ STR
end
private
def cacheable_column?(column)
- serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type)
- end
-
- # Define read method for serialized attribute.
- def define_read_method_for_serialized_attribute(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']"
- generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
+ attribute_types_cached_by_default.include?(column.type)
end
- # 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)
- cast_code = column.type_cast_code('v')
- access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
+ def internal_attribute_access_code(attr_name, cast_code)
+ access_code = "(v=@attributes[attr_name]) && #{cast_code}"
- unless attr_name.to_s == self.primary_key.to_s
- access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ unless attr_name == primary_key
+ access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ")
end
if cache_attribute?(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
+ access_code = "@attributes_cache[attr_name] ||= (#{access_code})"
end
- # Where possible, generate the method by evalling a string, as this will result in
- # faster accesses because it avoids the block eval and then string eval incurred
- # by the second branch.
- #
- # The second, slower, branch is necessary to support instances where the database
- # returns columns with extra stuff in (like 'my_column(omg)').
- if method_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
- def _#{method_name}
- #{access_code}
- end
-
- alias #{method_name} _#{method_name}
- STR
- else
- generated_attribute_methods.module_eval do
- define_method("_#{method_name}") { eval(access_code) }
- alias_method(method_name, "_#{method_name}")
- end
- end
+ "attr_name = '#{attr_name}'; #{access_code}"
end
- end
- # 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)
- method = "_#{attr_name}"
- if respond_to? method
- send method if @attributes.has_key?(attr_name.to_s)
- else
- _read_attribute attr_name
- end
- end
+ def external_attribute_access_code(attr_name, cast_code)
+ access_code = "v && #{cast_code}"
- def _read_attribute(attr_name)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
- value = @attributes[attr_name]
- unless value.nil?
- if column = column_for_attribute(attr_name)
- if unserializable_attribute?(attr_name, column)
- unserialize_attribute(attr_name)
- else
- column.type_cast(value)
+ if cache_attribute?(attr_name)
+ access_code = "attributes_cache[attr_name] ||= (#{access_code})"
end
- else
- value
+
+ access_code
end
- end
- end
- # Returns true if the attribute is of a text column and marked for serialization.
- def unserializable_attribute?(attr_name, column)
- column.text? && self.class.serialized_attributes.include?(attr_name)
+ def attribute_cast_code(attr_name)
+ columns_hash[attr_name].type_cast_code('v')
+ end
end
- # Returns the unserialized object of the attribute.
- def unserialize_attribute(attr_name)
- coder = self.class.serialized_attributes[attr_name]
- unserialized_object = coder.load(@attributes[attr_name])
-
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
+ # 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)
+ self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache)
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
new file mode 100644
index 0000000000..0c8e4e4b9a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -0,0 +1,93 @@
+module ActiveRecord
+ module AttributeMethods
+ module Serialization
+ extend ActiveSupport::Concern
+
+ included do
+ # Returns a hash of all the attributes that have been specified for serialization as
+ # keys and their class restriction as values.
+ config_attribute :serialized_attributes
+ self.serialized_attributes = {}
+ end
+
+ class Attribute < Struct.new(:coder, :value, :state)
+ def unserialized_value
+ state == :serialized ? unserialize : value
+ end
+
+ def serialized_value
+ state == :unserialized ? serialize : value
+ end
+
+ def unserialize
+ self.state = :unserialized
+ self.value = coder.load(value)
+ end
+
+ def serialize
+ self.state = :serialized
+ self.value = coder.dump(value)
+ end
+ end
+
+ module ClassMethods
+ # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
+ # then specify the name of that attribute using this method and it will be handled automatically.
+ # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
+ # class on retrieval or SerializationTypeMismatch will be raised.
+ #
+ # ==== Parameters
+ #
+ # * +attr_name+ - The field name that should be serialized.
+ # * +class_name+ - Optional, class name that the object type should be equal to.
+ #
+ # ==== Example
+ # # Serialize a preferences attribute
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ def serialize(attr_name, class_name = Object)
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
+ class_name
+ else
+ Coders::YAMLColumn.new(class_name)
+ end
+
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
+ # has its own hash of own serialized attributes
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
+ end
+
+ def initialize_attributes(attributes) #:nodoc:
+ super
+
+ serialized_attributes.each do |key, coder|
+ if attributes.key?(key)
+ attributes[key] = Attribute.new(coder, attributes[key], :serialized)
+ end
+ end
+
+ attributes
+ end
+
+ private
+
+ def attribute_cast_code(attr_name)
+ if serialized_attributes.include?(attr_name)
+ "v.unserialized_value"
+ else
+ super
+ end
+ end
+ end
+
+ def type_cast_attribute_for_write(column, value)
+ if column && coder = self.class.serialized_attributes[column.name]
+ Attribute.new(coder, value, :unserialized)
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 62a3cfa9a5..2f86e32f41 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -7,30 +7,25 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- cattr_accessor :time_zone_aware_attributes, :instance_writer => false
+ config_attribute :time_zone_aware_attributes, :global => true
self.time_zone_aware_attributes = false
- class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false
+ config_attribute :skip_time_zone_conversion_for_attributes
self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time
+ # The enhanced read method automatically converts the UTC time stored in the database to the time
# zone stored in Time.zone.
- def define_method_attribute(attr_name)
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
- method_body, line = <<-EOV, __LINE__ + 1
- def _#{attr_name}
- cached = @attributes_cache['#{attr_name}']
- return cached if cached
- time = _read_attribute('#{attr_name}')
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- end
- alias #{attr_name} _#{attr_name}
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
+ def attribute_cast_code(attr_name)
+ column = columns_hash[attr_name]
+
+ if create_time_zone_conversion_attribute?(attr_name, column)
+ typecast = "v = #{super}"
+ time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v"
+
+ "((#{typecast}) && (#{time_zone_conversion}))"
else
super
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index eb585ee906..fde55b95da 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -17,10 +17,6 @@ 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
@@ -32,10 +28,8 @@ module ActiveRecord
@attributes_cache.delete(attr_name)
column = column_for_attribute(attr_name)
- if column && column.number?
- @attributes[attr_name] = convert_number_column_value(value)
- elsif column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = value
+ if column || @attributes.has_key?(attr_name)
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
else
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
end
@@ -47,6 +41,26 @@ module ActiveRecord
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
+
+ def type_cast_attribute_for_write(column, value)
+ if column && column.number?
+ convert_number_column_value(value)
+ else
+ value
+ end
+ end
+
+ def convert_number_column_value(value)
+ if value == false
+ 0
+ elsif value == true
+ 1
+ elsif value.is_a?(String) && value.blank?
+ nil
+ else
+ value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 056170d82a..d468663084 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/array/wrap'
-
module ActiveRecord
# = Active Record Autosave Association
#
@@ -21,6 +19,21 @@ module ActiveRecord
# Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
# When the <tt>:autosave</tt> option is not present new associations are saved.
#
+ # == Validation
+ #
+ # Children records are validated unless <tt>:validate</tt> is +false+.
+ #
+ # == Callbacks
+ #
+ # Association with autosave option defines several callbacks on your
+ # model (before_save, after_create, after_update). Please note that
+ # callbacks are executed in the order they were defined in
+ # model. You should avoid modyfing the association content, before
+ # autosave callbacks are executed. Placing your callbacks after
+ # associations is usually a good practice.
+ #
+ # == Examples
+ #
# === One-to-one Example
#
# class Post
@@ -109,10 +122,7 @@ module ActiveRecord
# Now it _is_ removed from the database:
#
# Comment.find_by_id(id).nil? # => true
- #
- # === Validation
- #
- # Children records are validated unless <tt>:validate</tt> is +false+.
+
module AutosaveAssociation
extend ActiveSupport::Concern
@@ -264,7 +274,7 @@ module ActiveRecord
# turned on for the association.
def validate_single_association(reflection)
association = association_instance_get(reflection.name)
- record = association && association.target
+ record = association && association.reader
association_valid?(reflection, record) if record
end
@@ -285,7 +295,7 @@ module ActiveRecord
def association_valid?(reflection, record)
return true if record.destroyed? || record.marked_for_destruction?
- unless valid = record.valid?
+ unless valid = record.valid?(validation_context)
if reflection.options[:autosave]
record.errors.each do |attribute, message|
attribute = "#{reflection.name}.#{attribute}"
@@ -331,7 +341,7 @@ module ActiveRecord
if autosave
saved = association.insert_record(record, false)
else
- association.insert_record(record)
+ association.insert_record(record) unless reflection.nested?
end
elsif autosave
saved = record.save(:validate => false)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 7ba67b8540..d4d0220fb7 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,8 +1,3 @@
-begin
- require 'psych'
-rescue LoadError
-end
-
require 'yaml'
require 'set'
require 'active_support/benchmarkable'
@@ -12,7 +7,6 @@ require 'active_support/time'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
-require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/indifferent_access'
@@ -23,9 +17,11 @@ require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/blank'
+require 'active_support/deprecation'
require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
+require 'active_record/explain_subscriber'
module ActiveRecord #:nodoc:
# = Active Record
@@ -325,1845 +321,8 @@ module ActiveRecord #:nodoc:
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
# instances in the current object space.
class Base
- ##
- # :singleton-method:
- # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
- # which is then passed on to any new database connections made and which can be retrieved on both
- # a class and instance level by calling +logger+.
- cattr_accessor :logger, :instance_writer => false
-
- ##
- # :singleton-method:
- # Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
- #
- # For example, the following database.yml...
- #
- # development:
- # adapter: sqlite3
- # database: db/development.sqlite3
- #
- # production:
- # adapter: sqlite3
- # database: db/production.sqlite3
- #
- # ...would result in ActiveRecord::Base.configurations to look like this:
- #
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
- cattr_accessor :configurations, :instance_writer => false
- @@configurations = {}
-
- ##
- # :singleton-method:
- # Accessor for the prefix type that will be prepended to every primary key column name.
- # The options are :table_name and :table_name_with_underscore. If the first is specified,
- # the Product class will look for "productid" instead of "id" as the primary column. If the
- # latter is specified, the Product class will look for "product_id" instead of "id". Remember
- # that this is a global setting for all Active Records.
- cattr_accessor :primary_key_prefix_type, :instance_writer => false
- @@primary_key_prefix_type = nil
-
- ##
- # :singleton-method:
- # Accessor for the name of the prefix string to prepend to every table name. So if set
- # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
- # etc. This is a convenient way of creating a namespace for tables in a shared database.
- # By default, the prefix is the empty string.
- #
- # If you are organising your models within modules you can add a prefix to the models within
- # a namespace by defining a singleton method in the parent module called table_name_prefix which
- # returns your chosen prefix.
- class_attribute :table_name_prefix, :instance_writer => false
- self.table_name_prefix = ""
-
- ##
- # :singleton-method:
- # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
- # "people_basecamp"). By default, the suffix is the empty string.
- class_attribute :table_name_suffix, :instance_writer => false
- self.table_name_suffix = ""
-
- ##
- # :singleton-method:
- # 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.
- class_attribute :pluralize_table_names, :instance_writer => false
- self.pluralize_table_names = true
-
- ##
- # :singleton-method:
- # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
- # dates and times from the database. This is set to :local by default.
- cattr_accessor :default_timezone, :instance_writer => false
- @@default_timezone = :local
-
- ##
- # :singleton-method:
- # Specifies the format to use when dumping the database schema with Rails'
- # Rakefile. If :sql, the schema is dumped as (potentially database-
- # specific) SQL statements. If :ruby, the schema is dumped as an
- # ActiveRecord::Schema file which can be loaded into any database that
- # supports migrations. Use :ruby if you want to have different database
- # adapters for, e.g., your development and test environments.
- cattr_accessor :schema_format , :instance_writer => false
- @@schema_format = :ruby
-
- ##
- # :singleton-method:
- # Specify whether or not to use timestamps for migration versions
- cattr_accessor :timestamped_migrations , :instance_writer => false
- @@timestamped_migrations = true
-
- # Determine whether to store the full constant name including namespace when using STI
- class_attribute :store_full_sti_class
- self.store_full_sti_class = true
-
- # Stores the default scope for the class
- class_attribute :default_scopes, :instance_writer => false
- self.default_scopes = []
-
- # Returns a hash of all the attributes that have been specified for serialization as
- # keys and their class restriction as values.
- class_attribute :serialized_attributes
- self.serialized_attributes = {}
-
- class_attribute :_attr_readonly, :instance_writer => false
- self._attr_readonly = []
-
- 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, :uniq, :to => :scoped
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
-
- # Executes a custom SQL query against your database and returns all the results. The results will
- # be returned as an array with columns requested encapsulated as attributes of the model you call
- # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
- #
- # If you call a complicated SQL query which spans multiple tables the columns specified by the
- # SELECT will be attributes of the model, whether or not they are columns of the corresponding
- # table.
- #
- # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
- # no database agnostic conversions performed. This should be a last resort because using, for example,
- # MySQL specific terms will lock you to using that particular database engine or require you to
- # change your call if you switch engines.
- #
- # ==== Examples
- # # A simple SQL query spanning multiple tables
- # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
- # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
- #
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
- # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
- def find_by_sql(sql, binds = [])
- connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
- end
-
- # Creates an object (or multiple objects) and saves it to the database, if validations pass.
- # The resulting object is returned whether the object was saved successfully to the database or not.
- #
- # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
- # attributes on the objects that are to be created.
- #
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
- # ==== Examples
- # # Create a single new object
- # User.create(:first_name => 'Jamie')
- #
- # # Create a single new object using the :admin mass-assignment security role
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Create a single new object bypassing mass-assignment security
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- #
- # # Create an Array of new objects
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
- #
- # # Create a single object and pass it into a block to set other attributes.
- # User.create(:first_name => 'Jamie') do |u|
- # u.is_admin = false
- # end
- #
- # # Creating an Array of new objects using a block, where the block is executed for each object:
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
- # u.is_admin = false
- # end
- def create(attributes = nil, options = {}, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr, options, &block) }
- else
- object = new(attributes, options, &block)
- object.save
- object
- end
- end
-
- # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
- # The use of this method should be restricted to complicated SQL queries that can't be executed
- # using the ActiveRecord::Calculations class methods. Look into those before using this.
- #
- # ==== Parameters
- #
- # * +sql+ - An SQL statement which should return a count query from the database, see the example below.
- #
- # ==== Examples
- #
- # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
- def count_by_sql(sql)
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
- end
-
- # Attributes listed as readonly will be used to create a new record but update operations will
- # ignore these fields.
- def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
- end
-
- # Returns an array of all the attributes that have been specified as readonly.
- def readonly_attributes
- self._attr_readonly
- end
-
- # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
- # then specify the name of that attribute using this method and it will be handled automatically.
- # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
- # class on retrieval or SerializationTypeMismatch will be raised.
- #
- # ==== Parameters
- #
- # * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
- #
- # ==== Example
- # # Serialize a preferences attribute
- # class User < ActiveRecord::Base
- # serialize :preferences
- # end
- def serialize(attr_name, class_name = Object)
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
- else
- Coders::YAMLColumn.new(class_name)
- end
-
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
-
- # Guesses the table name (in forced lower-case) based on the name of the class in the
- # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
- # looks like: Reply < Message < ActiveRecord::Base, then Message is used
- # to guess the table name even when called on Reply. The rules used to do the guess
- # are handled by the Inflector class in Active Support, which knows almost all common
- # English inflections. You can add new inflections in config/initializers/inflections.rb.
- #
- # Nested classes are given table names prefixed by the singular form of
- # the parent's table name. Enclosing modules are not considered.
- #
- # ==== Examples
- #
- # class Invoice < ActiveRecord::Base
- # end
- #
- # file class table_name
- # invoice.rb Invoice invoices
- #
- # 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
- #
- # file class table_name
- # invoice/lineitem.rb Invoice::Lineitem lineitems
- #
- # Additionally, the class-level +table_name_prefix+ is prepended and the
- # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
- # the table name guess for an Invoice class becomes "myapp_invoices".
- # Invoice::Lineitem becomes "myapp_invoice_lineitems".
- #
- # You can also overwrite this class method to allow for unguessable
- # links, such as a Mouse class with a link to a "mice" table. Example:
- #
- # class Mouse < ActiveRecord::Base
- # set_table_name "mice"
- # end
- def table_name
- reset_table_name
- end
-
- # Returns a quoted version of the table name, used to construct SQL statements.
- def quoted_table_name
- @quoted_table_name ||= connection.quote_table_name(table_name)
- end
-
- # 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
-
- def full_table_name_prefix #:nodoc:
- (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
- end
-
- # Defines the column name for use with single table inheritance. Use
- # <tt>set_inheritance_column</tt> to set a different value.
- def inheritance_column
- @inheritance_column ||= "type"
- end
-
- # Lazy-set the sequence name to the connection's default. This method
- # is only ever called once since set_sequence_name overrides it.
- def sequence_name #:nodoc:
- reset_sequence_name
- end
-
- def reset_sequence_name #:nodoc:
- default = connection.default_sequence_name(table_name, primary_key)
- set_sequence_name(default)
- default
- end
-
- # Sets the table name. If the value is nil or false then the value returned by the given
- # block is used.
- #
- # class Project < ActiveRecord::Base
- # set_table_name "project"
- # end
- def set_table_name(value = nil, &block)
- @quoted_table_name = nil
- define_attr_method :table_name, value, &block
- @arel_table = nil
-
- @relation = Relation.new(self, arel_table)
- end
- alias :table_name= :set_table_name
-
- # Sets the name of the inheritance column to use to the given value,
- # or (if the value # is nil or false) to the value returned by the
- # given block.
- #
- # class Project < ActiveRecord::Base
- # set_inheritance_column do
- # original_inheritance_column + "_id"
- # end
- # end
- def set_inheritance_column(value = nil, &block)
- define_attr_method :inheritance_column, value, &block
- end
- alias :inheritance_column= :set_inheritance_column
-
- # Sets the name of the sequence to use when generating ids to the given
- # value, or (if the value is nil or false) to the value returned by the
- # given block. This is required for Oracle and is useful for any
- # database which relies on sequences for primary key generation.
- #
- # If a sequence name is not explicitly set when using Oracle or Firebird,
- # it will default to the commonly used pattern of: #{table_name}_seq
- #
- # If a sequence name is not explicitly set when using PostgreSQL, it
- # will discover the sequence corresponding to your primary key for you.
- #
- # class Project < ActiveRecord::Base
- # set_sequence_name "projectseq" # default would have been "project_seq"
- # end
- def set_sequence_name(value = nil, &block)
- define_attr_method :sequence_name, value, &block
- end
- alias :sequence_name= :set_sequence_name
-
- # Indicates whether the table associated with this class exists
- def table_exists?
- connection.table_exists?(table_name)
- end
-
- # Returns an array of column objects for the table associated with this class.
- def columns
- if defined?(@primary_key)
- connection.schema_cache.primary_keys[table_name] ||= primary_key
- end
-
- connection.schema_cache.columns[table_name]
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- connection.schema_cache.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.schema_cache.column_defaults[table_name]
- end
-
- # Returns an array of column names as strings.
- def column_names
- @column_names ||= columns.map { |column| column.name }
- end
-
- # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
- # and columns used for single table inheritance have been removed.
- def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
- end
-
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- methods
- end
- end
-
- # Resets all the cached information about columns, which will cause them
- # to be reloaded on the next request.
- #
- # The most common usage pattern for this method is probably in a migration,
- # when just after creating a table you want to populate it with some default
- # values, eg:
- #
- # class CreateJobLevels < ActiveRecord::Migration
- # def up
- # create_table :job_levels do |t|
- # t.integer :id
- # t.string :name
- #
- # t.timestamps
- # end
- #
- # JobLevel.reset_column_information
- # %w{assistant executive manager director}.each do |type|
- # JobLevel.create(:name => type)
- # end
- # end
- #
- # def down
- # drop_table :job_levels
- # end
- # end
- def reset_column_information
- connection.clear_cache!
- undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name) if table_exists?
-
- @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
- @arel_engine = @relation = nil
- end
-
- def clear_cache! # :nodoc:
- connection.schema_cache.clear!
- end
-
- def attribute_method?(attribute)
- 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
- classes = [klass]
- return classes if klass == ActiveRecord::Base
-
- while klass != klass.base_class
- classes << klass = klass.superclass
- end
- classes
- end
-
- # Set the i18n scope to overwrite ActiveModel.
- def i18n_scope #:nodoc:
- :activerecord
- end
-
- # True if this isn't a concrete subclass needing a STI type condition.
- def descends_from_active_record?
- if superclass.abstract_class?
- superclass.descends_from_active_record?
- else
- superclass == Base || !columns_hash.include?(inheritance_column)
- end
- end
-
- def finder_needs_type_condition? #:nodoc:
- # This is like this because benchmarking justifies the strange :false stuff
- :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
- end
-
- # Returns a string like 'Post(id:integer, title:string, body:text)'
- def inspect
- if self == Base
- super
- elsif abstract_class?
- "#{super}(abstract)"
- elsif table_exists?
- attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
- "#{super}(#{attr_list})"
- else
- "#{super}(Table doesn't exist)"
- end
- end
-
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
- end
-
- # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
- connection.quote(object)
- end
-
- # Overwrite the default class equality method to provide support for association proxies.
- def ===(object)
- object.is_a?(self)
- end
-
- def symbolized_base_class
- @symbolized_base_class ||= base_class.to_s.to_sym
- end
-
- def symbolized_sti_name
- @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
- end
-
- # Returns the base AR subclass that this class descends from. If A
- # extends AR::Base, A.base_class will return A. If B descends from A
- # through some arbitrarily deep hierarchy, B.base_class will return A.
- #
- # If B < A and C < B and if A is an abstract_class then both B.base_class
- # and C.base_class would return B as the answer since A is an abstract_class.
- def base_class
- class_of_active_record_descendant(self)
- end
-
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- attr_accessor :abstract_class
-
- # Returns whether this class is an abstract class or not.
- def abstract_class?
- defined?(@abstract_class) && @abstract_class == true
- end
-
- def respond_to?(method_id, include_private = false)
- if match = DynamicFinderMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- elsif match = DynamicScopeMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- end
-
- super
- end
-
- def sti_name
- store_full_sti_class ? name : name.demodulize
- end
-
- def arel_table
- @arel_table ||= Arel::Table.new(table_name, arel_engine)
- end
-
- def arel_engine
- @arel_engine ||= begin
- if self == ActiveRecord::Base
- ActiveRecord::Base
- else
- connection_handler.connection_pools[name] ? self : superclass.arel_engine
- end
- end
- end
-
- # Returns a scope for this class without taking into account the default_scope.
- #
- # class Post < ActiveRecord::Base
- # def self.default_scope
- # where :published => true
- # end
- # end
- #
- # Post.all # Fires "SELECT * FROM posts WHERE published = true"
- # Post.unscoped.all # Fires "SELECT * FROM posts"
- #
- # This method also accepts a block meaning that all queries inside the block will
- # not use the default_scope:
- #
- # Post.unscoped {
- # 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>
- # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
- #
- # Post.unscoped.published
- # Post.published
- def unscoped #:nodoc:
- block_given? ? relation.scoping { yield } : relation
- end
-
- def before_remove_const #:nodoc:
- self.current_scope = nil
- 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.
- def instantiate(record)
- sti_class = find_sti_class(record[inheritance_column])
- record_id = sti_class.primary_key && record[sti_class.primary_key]
-
- if ActiveRecord::IdentityMap.enabled? && record_id
- if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
- record_id = record_id.to_i
- end
- if instance = IdentityMap.get(sti_class, record_id)
- instance.reinit_with('attributes' => record)
- else
- instance = sti_class.allocate.init_with('attributes' => record)
- IdentityMap.add(instance)
- end
- else
- instance = sti_class.allocate.init_with('attributes' => record)
- end
-
- instance
- end
-
- private
-
- def relation #:nodoc:
- @relation ||= Relation.new(self, arel_table)
-
- if finder_needs_type_condition?
- @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
- else
- @relation
- end
- end
-
- def find_sti_class(type_name)
- if type_name.blank? || !columns_hash.include?(inheritance_column)
- self
- else
- begin
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
- end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
- end
- end
-
- def construct_finder_arel(options = {}, scope = nil)
- relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
- relation = scope.merge(relation) if scope
- relation
- end
-
- def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
- sti_names = ([self] + descendants).map { |model| model.sti_name }
-
- sti_column.in(sti_names)
- end
-
- # Guesses the table name, but does not decorate it with prefix and suffix information.
- def undecorated_table_name(class_name = base_class.name)
- table_name = class_name.to_s.demodulize.underscore
- table_name = table_name.pluralize if pluralize_table_names
- table_name
- end
-
- # Computes and returns a table name according to default conventions.
- def compute_table_name
- base = base_class
- if self == base
- # Nested classes are prefixed with singular parent table name.
- if parent < ActiveRecord::Base && !parent.abstract_class?
- contained = parent.table_name
- contained = contained.singularize if parent.pluralize_table_names
- contained += '_'
- end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
- else
- # STI subclasses always use their superclass' table.
- base.table_name
- end
- end
-
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
- # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
- # section at the top of this file for more detailed information.
- #
- # It's even possible to use all the additional parameters to +find+. For example, the
- # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
- #
- # 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) || DynamicScopeMatch.match(method_id))
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- 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
- 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)]
- #
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
- 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
- end
- end
-
- # Similar in purpose to +expand_hash_conditions_for_aggregates+.
- def expand_attribute_names_for_aggregates(attribute_names)
- attribute_names.map { |attribute_name|
- unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
- aggregate_mapping(aggregation).map do |field_attr, _|
- field_attr.to_sym
- end
- else
- attribute_name.to_sym
- end
- }.flatten
- end
-
- def all_attributes_exists?(attribute_names)
- (expand_attribute_names_for_aggregates(attribute_names) -
- column_methods_hash.keys).empty?
- end
-
- protected
- # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
- # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
- # <tt>:create</tt> parameters are an attributes hash.
- #
- # class Article < ActiveRecord::Base
- # def self.create_with_scope
- # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
- # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
- # a = create(1)
- # a.blog_id # => 1
- # end
- # end
- # end
- #
- # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
- # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
- #
- # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
- # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
- # array of strings format for your joins.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_scope
- # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
- # with_scope(:find => limit(10)) do
- # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
- # end
- # with_scope(:find => where(:author_id => 3)) do
- # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
- # end
- # end
- # end
- # end
- #
- # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_exclusive_scope
- # with_scope(:find => where(:blog_id => 1).limit(1)) do
- # with_exclusive_scope(:find => limit(10)) do
- # all # => SELECT * from articles LIMIT 10
- # end
- # end
- # end
- # end
- #
- # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
- def with_scope(scope = {}, action = :merge, &block)
- # If another Active Record class has been passed in, get its current scope
- scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
-
- previous_scope = self.current_scope
-
- if scope.is_a?(Hash)
- # Dup first and second level of hash (method and params).
- scope = scope.dup
- scope.each do |method, params|
- scope[method] = params.dup unless params == true
- end
-
- scope.assert_valid_keys([ :find, :create ])
- relation = construct_finder_arel(scope[:find] || {})
- relation.default_scoped = true unless action == :overwrite
-
- if previous_scope && previous_scope.create_with_value && scope[:create]
- scope_for_create = if action == :merge
- previous_scope.create_with_value.merge(scope[:create])
- else
- scope[:create]
- end
-
- relation = relation.create_with(scope_for_create)
- else
- scope_for_create = scope[:create]
- scope_for_create ||= previous_scope.create_with_value if previous_scope
- relation = relation.create_with(scope_for_create) if scope_for_create
- end
-
- scope = relation
- end
-
- scope = previous_scope.merge(scope) if previous_scope && action == :merge
-
- self.current_scope = scope
- begin
- yield
- ensure
- self.current_scope = previous_scope
- end
- end
-
- # Works like with_scope, but discards any nested properties.
- def with_exclusive_scope(method_scoping = {}, &block)
- if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
- raise ArgumentError, <<-MSG
-New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
-
- User.unscoped.where(:active => true)
-
-Or call unscoped with a block:
-
- User.unscoped do
- User.where(:active => true).all
- end
-
-MSG
- end
- with_scope(method_scoping, :overwrite, &block)
- end
-
- def current_scope #:nodoc:
- Thread.current["#{self}_current_scope"]
- end
-
- def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_current_scope"] = scope
- end
-
- # Use this macro in your model to set a default scope for all operations on
- # the model.
- #
- # class Article < ActiveRecord::Base
- # default_scope where(:published => true)
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true
- #
- # The <tt>default_scope</tt> is also applied while creating/building a record. It is not
- # applied while updating a record.
- #
- # Article.new.published # => true
- # Article.create.published # => true
- #
- # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
- #
- # class Article < ActiveRecord::Base
- # default_scope { where(:published_at => Time.now - 1.week) }
- # end
- #
- # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
- # macro, and it will be called when building the default scope.)
- #
- # If you use multiple <tt>default_scope</tt> declarations in your model then they will
- # be merged together:
- #
- # class Article < ActiveRecord::Base
- # default_scope where(:published => true)
- # default_scope where(:rating => 'G')
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
- #
- # This is also the case with inheritance and module includes where the parent or module
- # defines a <tt>default_scope</tt> and the child or including class defines a second one.
- #
- # If you need to do more complex things with a default scope, you can alternatively
- # define it as a class method:
- #
- # class Article < ActiveRecord::Base
- # def self.default_scope
- # # Should return a scope, you can call 'super' here etc.
- # end
- # end
- def default_scope(scope = {})
- scope = Proc.new if block_given?
- self.default_scopes = default_scopes + [scope]
- end
-
- def build_default_scope #:nodoc:
- if method(:default_scope).owner != Base.singleton_class
- evaluate_default_scope { default_scope }
- elsif default_scopes.any?
- 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)
- if type_name.match(/^::/)
- # If the type is prefixed with a scope operator then we assume that
- # the type_name is an absolute reference.
- ActiveSupport::Dependencies.constantize(type_name)
- else
- # Build a list of candidates to search for
- candidates = []
- name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
- candidates << type_name
-
- candidates.each do |candidate|
- begin
- constant = ActiveSupport::Dependencies.constantize(candidate)
- return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
- end
- end
-
- raise NameError, "uninitialized constant #{candidates.first}"
- end
- end
-
- # 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 == 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"
- else
- class_of_active_record_descendant(klass.superclass)
- end
- end
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
- return nil if condition.blank?
-
- case condition
- when Array; sanitize_sql_array(condition)
- when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
- else condition
- end
- end
- alias_method :sanitize_sql, :sanitize_sql_for_conditions
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a SET clause.
- # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
- case assignments
- when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
- else assignments
- end
- end
-
- def aggregate_mapping(reflection)
- mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
- mapping.first.is_a?(Array) ? mapping : [mapping]
- end
-
- # Accepts a hash of SQL conditions and replaces those attributes
- # that correspond to a +composed_of+ relationship with their expanded
- # aggregate attribute values.
- # Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, :class_name => "Address",
- # :mapping => [%w(address_street street), %w(address_city city)]
- # end
- # Then:
- # { :address => Address.new("813 abc st.", "chicago") }
- # # => { :address_street => "813 abc st.", :address_city => "chicago" }
- def expand_hash_conditions_for_aggregates(attrs)
- expanded_attrs = {}
- attrs.each do |attr, value|
- unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
- mapping = aggregate_mapping(aggregation)
- mapping.each do |field_attr, aggregate_attr|
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
- expanded_attrs[field_attr] = value
- else
- expanded_attrs[field_attr] = value.send(aggregate_attr)
- end
- end
- else
- expanded_attrs[attr] = value
- end
- end
- expanded_attrs
- end
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
- # { :name => "foo'bar", :group_id => 4 }
- # # => "name='foo''bar' and group_id= 4"
- # { :status => nil, :group_id => [1,2,3] }
- # # => "status IS NULL and group_id IN (1,2,3)"
- # { :age => 13..18 }
- # # => "age BETWEEN 13 AND 18"
- # { 'other_records.id' => 7 }
- # # => "`other_records`.`id` = 7"
- # { :other_records => { :id => 7 } }
- # # => "`other_records`.`id` = 7"
- # And for value objects on a composed_of relationship:
- # { :address => Address.new("123 abc st.", "chicago") }
- # # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
- attrs = expand_hash_conditions_for_aggregates(attrs)
-
- table = Arel::Table.new(table_name).alias(default_table_name)
- PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
- connection.visitor.accept b
- }.join(' AND ')
- end
- alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { :status => nil, :group_id => 1 }
- # # => "status = NULL , group_id = 1"
- def sanitize_sql_hash_for_assignment(attrs)
- attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
- end.join(', ')
- end
-
- # Accepts an array of conditions. The array has each value
- # sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_array(ary)
- statement, *values = ary
- if values.first.is_a?(Hash) && statement =~ /:\w+/
- replace_named_bind_variables(statement, values.first)
- elsif statement.include?('?')
- replace_bind_variables(statement, values)
- elsif statement.blank?
- statement
- else
- statement % values.collect { |value| connection.quote_string(value.to_s) }
- end
- end
-
- alias_method :sanitize_conditions, :sanitize_sql
-
- def replace_bind_variables(statement, values) #:nodoc:
- raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
- bound = values.dup
- c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
- end
-
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do
- if $1 == ':' # skip postgresql casts
- $& # return the whole match
- elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
- else
- raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
- end
- end
- end
-
- def expand_range_bind_variables(bind_vars) #:nodoc:
- expanded = []
-
- bind_vars.each do |var|
- next if var.is_a?(Hash)
-
- if var.is_a?(Range)
- expanded << var.first
- expanded << var.last
- else
- expanded << var
- end
- end
-
- expanded
- end
-
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
- if value.respond_to?(:empty?) && value.empty?
- c.quote(nil)
- else
- value.map { |v| c.quote(v) }.join(',')
- end
- else
- c.quote(value)
- end
- end
-
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
- unless expected == provided
- raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
- end
- end
-
- def encode_quoted_value(value) #:nodoc:
- quoted_value = connection.quote(value)
- quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
- quoted_value
- end
- end
-
- public
- # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
- # attributes but not yet saved (pass a hash with key names matching the associated table column names).
- # In both instances, valid attribute keys are determined by the column names of the associated table --
- # hence you can't have attributes that aren't part of the table columns.
- #
- # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
- # ==== Examples
- # # Instantiates a single new object
- # User.new(:first_name => 'Jamie')
- #
- # # Instantiates a single new object using the :admin mass-assignment security role
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Instantiates a single new object bypassing mass-assignment security
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- def initialize(attributes = nil, options = {})
- @attributes = attributes_from_column_definition
- @association_cache = {}
- @aggregation_cache = {}
- @attributes_cache = {}
- @new_record = true
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @previously_changed = {}
- @changed_attributes = {}
- @relation = nil
-
- ensure_proper_type
- set_serialized_attributes
-
- populate_with_current_scope_attributes
-
- assign_attributes(attributes, options) if attributes
-
- yield self if block_given?
- run_callbacks :initialize
- end
-
- # Populate +coder+ with attributes about this record that should be
- # serialized. The structure of +coder+ defined in this method is
- # guaranteed to match the structure of +coder+ passed to the +init_with+
- # method.
- #
- # Example:
- #
- # class Post < ActiveRecord::Base
- # end
- # coder = {}
- # Post.new.encode_with(coder)
- # coder # => { 'id' => nil, ... }
- def encode_with(coder)
- coder['attributes'] = attributes
- end
-
- # Initialize an empty model object from +coder+. +coder+ must contain
- # the attributes necessary for initializing an empty model object. For
- # example:
- #
- # class Post < ActiveRecord::Base
- # end
- #
- # post = Post.allocate
- # post.init_with('attributes' => { 'title' => 'hello world' })
- # post.title # => 'hello world'
- def init_with(coder)
- @attributes = coder['attributes']
- @relation = nil
-
- set_serialized_attributes
-
- @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
- @association_cache = {}
- @aggregation_cache = {}
- @readonly = @destroyed = @marked_for_destruction = false
- @new_record = false
- run_callbacks :find
- run_callbacks :initialize
-
- self
- 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.
- #
- # For example, suppose that you have a User model, and that you have a
- # <tt>resources :users</tt> route. Normally, +user_path+ will
- # construct a path with the user object's 'id' in it:
- #
- # user = User.find_by_name('Phusion')
- # user_path(user) # => "/users/1"
- #
- # You can override +to_param+ in your model to make +user_path+ construct
- # a path using the user's name instead of the user's id:
- #
- # class User < ActiveRecord::Base
- # def to_param # overridden
- # name
- # end
- # end
- #
- # user = User.find_by_name('Phusion')
- # user_path(user) # => "/users/Phusion"
- def to_param
- # We can't use alias_method here, because method 'id' optimizes itself on the fly.
- id && id.to_s # Be sure to stringify the id for routes
- end
-
- # Returns a cache key that can be used to identify this record.
- #
- # ==== Examples
- #
- # Product.new.cache_key # => "products/new"
- # Product.find(5).cache_key # => "products/5" (updated_at not available)
- # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
- case
- when new_record?
- "#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
- timestamp = timestamp.utc.to_s(:number)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
- else
- "#{self.class.model_name.cache_key}/#{id}"
- end
- end
-
- def quoted_id #:nodoc:
- quote_value(id, column_for_attribute(self.class.primary_key))
- end
-
- # Returns true if the given attribute is in the attributes hash
- def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
- end
-
- # Returns an array of names for the attributes available on this object.
- def attribute_names
- @attributes.keys
- end
-
- # Allows you to set all the attributes at once by passing in a hash with keys
- # matching the attribute names (which again matches the column names).
- #
- # If any attributes are protected by either +attr_protected+ or
- # +attr_accessible+ then only settable attributes will be assigned.
- #
- # class User < ActiveRecord::Base
- # attr_protected :is_admin
- # end
- #
- # user = User.new
- # user.attributes = { :username => 'Phusion', :is_admin => true }
- # user.username # => "Phusion"
- # user.is_admin? # => false
- def attributes=(new_attributes)
- return unless new_attributes.is_a?(Hash)
-
- assign_attributes(new_attributes)
- end
-
- # Allows you to set all the attributes for a particular mass-assignment
- # security role by passing in a hash of attributes with keys matching
- # the attribute names (which again matches the column names) and the role
- # name using the :as option.
- #
- # To bypass mass-assignment security you can use the :without_protection => true
- # option.
- #
- # class User < ActiveRecord::Base
- # attr_accessible :name
- # attr_accessible :name, :is_admin, :as => :admin
- # end
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true })
- # user.name # => "Josh"
- # user.is_admin? # => false
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
- # user.name # => "Josh"
- # user.is_admin? # => true
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
- # user.name # => "Josh"
- # user.is_admin? # => true
- def assign_attributes(new_attributes, options = {})
- return unless new_attributes
-
- attributes = new_attributes.stringify_keys
- multi_parameter_attributes = []
- @mass_assignment_options = options
-
- unless options[:without_protection]
- attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
- end
-
- attributes.each do |k, v|
- if k.include?("(")
- multi_parameter_attributes << [ k, v ]
- elsif respond_to?("#{k}=")
- send("#{k}=", v)
- else
- raise(UnknownAttributeError, "unknown attribute: #{k}")
- end
- end
-
- @mass_assignment_options = nil
- assign_multiparameter_attributes(multi_parameter_attributes)
- end
-
- # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
- def attributes
- Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
- end
-
- # Returns an <tt>#inspect</tt>-like string for the value of the
- # 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.
- #
- # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
- #
- # person.attribute_for_inspect(:name)
- # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
- #
- # person.attribute_for_inspect(:created_at)
- # # => '"2009-01-12 04:48:57"'
- def attribute_for_inspect(attr_name)
- value = read_attribute(attr_name)
-
- if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
- elsif value.is_a?(Date) || value.is_a?(Time)
- %("#{value.to_s(:db)}")
- else
- value.inspect
- end
- end
-
- # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
- # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
- def attribute_present?(attribute)
- value = _read_attribute(attribute)
- !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
- end
-
- # Returns the column object for the named attribute.
- def column_for_attribute(name)
- self.class.columns_hash[name.to_s]
- end
-
- # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
- # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
- #
- # Note that new records are different from any other record by definition, unless the
- # other record is the receiver itself. Besides, if you fetch existing records with
- # +select+ and leave the ID out, you're on your own, this predicate will return false.
- #
- # Note also that destroying a record preserves its ID in the model instance, so deleted
- # models are still comparable.
- def ==(comparison_object)
- super ||
- comparison_object.instance_of?(self.class) &&
- id.present? &&
- comparison_object.id == id
- 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) ]
- def hash
- id.hash
- end
-
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
- def freeze
- @attributes.freeze; self
- end
-
- # Returns +true+ if the attributes hash has been frozen.
- def frozen?
- @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:
- copy = super
- copy.initialize_dup(self)
- copy
- end
- end
-
- # Duped objects have no id assigned and are treated as new records. Note
- # that this is a "shallow" copy as it copies the object's attributes
- # only, not its associations. The extent of a "deep" copy is application
- # specific and is therefore left to the application to implement according
- # to its need.
- # The dup method does not preserve the timestamps (created|updated)_(at|on).
- def initialize_dup(other)
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- cloned_attributes.delete(self.class.primary_key)
-
- @attributes = cloned_attributes
-
- _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
-
- @changed_attributes = {}
- attributes_from_column_definition.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
- end
-
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @new_record = true
-
- ensure_proper_type
- populate_with_current_scope_attributes
- super
- end
-
- # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
- # attributes will be marked as read only since they cannot be saved.
- def readonly?
- @readonly
- end
-
- # Marks this record as read only.
- def readonly!
- @readonly = true
- end
-
- # Returns the contents of the record as a nicely formatted string.
- def inspect
- 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
- def clone_attributes(reader_method = :read_attribute, attributes = {})
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
- def clone_attribute_value(reader_method, attribute_name)
- value = send(reader_method, attribute_name)
- value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- 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
- sattrs = self.class.serialized_attributes
-
- sattrs.each do |key, coder|
- @attributes[key] = coder.load @attributes[key] if @attributes.key?(key)
- end
- end
-
- # Sets the attribute used for single table inheritance to this class name if this is not the
- # ActiveRecord::Base descendant.
- # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
- # 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
- klass = self.class
- if klass.finder_needs_type_condition?
- write_attribute(klass.inheritance_column, klass.sti_name)
- end
- end
-
- # The primary key and inheritance column can never be set by mass-assignment for security reasons.
- def self.attributes_protected_by_default
- default = [ primary_key, inheritance_column ]
- default << 'id' unless primary_key.eql? 'id'
- default
- end
-
- # Returns a copy of the attributes hash where all the values have been safely quoted for use in
- # an Arel insert/update method.
- def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
- klass = self.class
- arel_table = klass.arel_table
-
- attribute_names.each do |name|
- if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
-
- if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
-
- value = if coder = klass.serialized_attributes[name]
- coder.dump @attributes[name]
- else
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already type
- # casted, this code could be simplified
- read_attribute(name)
- end
-
- attrs[arel_table[name]] = value
- end
- end
- end
- attrs
- end
-
- # Quote strings appropriately for SQL statements.
- def quote_value(value, column = nil)
- self.class.connection.quote(value, column)
- end
-
- # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
- # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
- # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
- # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to nil.
- def assign_multiparameter_attributes(pairs)
- execute_callstack_for_multiparameter_attributes(
- extract_callstack_for_multiparameter_attributes(pairs)
- )
- end
-
- def instantiate_time_object(name, values)
- if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
- Time.zone.local(*values)
- else
- Time.time_with_datetime_fallback(@@default_timezone, *values)
- end
- end
-
- def execute_callstack_for_multiparameter_attributes(callstack)
- errors = []
- callstack.each do |name, values_with_empty_parameters|
- begin
- send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
- rescue => ex
- errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
- end
- end
- unless errors.empty?
- raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
- end
- end
-
- def read_value_from_parameter(name, values_hash_from_param)
- klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
- if values_hash_from_param.values.all?{|v|v.nil?}
- nil
- elsif klass == Time
- read_time_parameter_value(name, values_hash_from_param)
- elsif klass == Date
- read_date_parameter_value(name, values_hash_from_param)
- else
- read_other_parameter_value(klass, name, values_hash_from_param)
- end
- end
-
- def read_time_parameter_value(name, values_hash_from_param)
- # 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 Time bits are not there, then default to 0
- (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)
- 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 # 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
-
- def read_other_parameter_value(klass, name, values_hash_from_param)
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
- values = (1..max_position).collect do |position|
- raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
- values_hash_from_param[position]
- end
- klass.new(*values)
- end
-
- def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
- [values_hash_from_param.keys.max,upper_cap].min
- end
-
- def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
-
- pairs.each do |pair|
- multiparameter_name, value = pair
- attribute_name = multiparameter_name.split("(").first
- attributes[attribute_name] = {} unless attributes.include?(attribute_name)
-
- parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
- attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
- end
-
- attributes
- end
-
- def type_cast_attribute_value(multiparameter_name, value)
- multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
- end
-
- def find_parameter_position(multiparameter_name)
- multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
- end
-
- # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
- def comma_pair_list(hash)
- hash.map { |k,v| "#{k} = #{v}" }.join(", ")
- end
-
- def quote_columns(quoter, hash)
- Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }]
- end
-
- def quoted_comma_pair_list(quoter, hash)
- comma_pair_list(quote_columns(quoter, hash))
- end
-
- def convert_number_column_value(value)
- if value == false
- 0
- elsif value == true
- 1
- elsif value.is_a?(String) && value.blank?
- nil
- else
- value
- end
- end
-
- def populate_with_current_scope_attributes
- return unless self.class.scope_attributes?
-
- self.class.scope_attributes.each do |att,value|
- send("#{att}=", value) if respond_to?("#{att}=")
- end
- end
- end
-
- Base.class_eval do
- include ActiveRecord::Persistence
- extend ActiveModel::Naming
- extend QueryCache::ClassMethods
- extend ActiveSupport::Benchmarkable
- extend ActiveSupport::DescendantsTracker
-
- include ActiveModel::Conversion
- include Validations
- extend CounterCache
- include Locking::Optimistic, Locking::Pessimistic
- include AttributeMethods
- include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query
- include AttributeMethods::PrimaryKey
- include AttributeMethods::TimeZoneConversion
- include AttributeMethods::Dirty
- include ActiveModel::MassAssignmentSecurity
- include Callbacks, ActiveModel::Observing, Timestamp
- include Associations, NamedScope
- include IdentityMap
- include ActiveModel::SecurePassword
-
- # AutosaveAssociation needs to be included before Transactions, because we want
- # #save_with_autosave_associations to be wrapped inside a transaction.
- include AutosaveAssociation, NestedAttributes
- include Aggregations, Transactions, Reflection, Serialization, Store
-
- NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner)
-
- # 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)).
- # (Alias for the protected read_attribute method).
- alias [] read_attribute
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
- alias []= write_attribute
-
- public :[], :[]=
+ include ActiveRecord::Model
end
end
-require 'active_record/connection_adapters/abstract/connection_specification'
-ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
+ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index a175bf003c..a050fabf35 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/array/wrap'
-
module ActiveRecord
# = Active Record Callbacks
#
@@ -25,7 +23,7 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
- # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
+ # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
@@ -215,24 +213,48 @@ module ActiveRecord
# instead of quietly returning +false+.
#
# == Debugging callbacks
- #
- # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
+ #
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
- #
- # To find all callbacks in the before_save callback chain:
- #
+ #
+ # To find all callbacks in the before_save callback chain:
+ #
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
- #
+ #
# Returns an array of callback objects that form the before_save chain.
- #
+ #
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
- #
+ #
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
- #
+ #
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
- #
+ #
module Callbacks
+ # We can't define callbacks directly on ActiveRecord::Model because
+ # it is a module. So we queue up the definitions and execute them
+ # when ActiveRecord::Model is included.
+ module Register #:nodoc:
+ def self.extended(base)
+ base.config_attribute :_callbacks_register
+ base._callbacks_register = []
+ end
+
+ def self.setup(base)
+ base._callbacks_register.each do |item|
+ base.send(*item)
+ end
+ end
+
+ def define_callbacks(*args)
+ self._callbacks_register << [:define_callbacks, *args]
+ end
+
+ def define_model_callbacks(*args)
+ self._callbacks_register << [:define_model_callbacks, *args]
+ end
+ end
+
extend ActiveSupport::Concern
CALLBACKS = [
@@ -242,8 +264,11 @@ module ActiveRecord
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
+ module ClassMethods
+ include ActiveModel::Callbacks
+ end
+
included do
- extend ActiveModel::Callbacks
include ActiveModel::Validations::Callbacks
define_model_callbacks :initialize, :find, :touch, :only => :after
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 e32154780a..d69f02d504 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,6 @@
require 'thread'
require 'monitor'
require 'set'
-require 'active_support/core_ext/module/synchronization'
require 'active_support/core_ext/module/deprecation'
module ActiveRecord
@@ -10,6 +9,13 @@ module ActiveRecord
class ConnectionTimeoutError < ConnectionNotEstablished
end
+ # Raised when a connection pool is full and another connection is requested
+ class PoolFullError < ConnectionNotEstablished
+ def initialize size, timeout
+ super("Connection pool of size #{size} and timeout #{timeout}s is full")
+ end
+ end
+
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -58,8 +64,35 @@ module ActiveRecord
# * +wait_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
class ConnectionPool
- attr_accessor :automatic_reconnect
- attr_reader :spec, :connections
+ # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
+ # A reaper instantiated with a nil frequency will never reap the
+ # connection pool.
+ #
+ # Configure the frequency by setting "reaping_frequency" in your
+ # database yaml file.
+ class Reaper
+ attr_reader :pool, :frequency
+
+ def initialize(pool, frequency)
+ @pool = pool
+ @frequency = frequency
+ end
+
+ def run
+ return unless frequency
+ Thread.new(frequency, pool) { |t, p|
+ while true
+ sleep t
+ p.reap
+ end
+ }
+ end
+ end
+
+ include MonitorMixin
+
+ attr_accessor :automatic_reconnect, :timeout
+ attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -68,23 +101,22 @@ module ActiveRecord
#
# The default ConnectionPool maximum size is 5.
def initialize(spec)
+ super()
+
@spec = spec
# The cache of reserved connections mapped to threads
@reserved_connections = {}
- # The mutex used to synchronize pool access
- @connection_mutex = Monitor.new
- @queue = @connection_mutex.new_cond
@timeout = spec.config[:wait_timeout] || 5
+ @reaper = Reaper.new self, spec.config[:reaping_frequency]
+ @reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
- @checked_out = []
@automatic_reconnect = true
- @visitor = nil
end
# Retrieve the connection associated with the current thread, or call
@@ -99,7 +131,7 @@ module ActiveRecord
# Check to see if there is an active connection in this connection
# pool.
def active_connection?
- @reserved_connections.key? current_connection_id
+ active_connections.any?
end
# Signal that the thread is finished with the current connection.
@@ -115,7 +147,7 @@ module ActiveRecord
# connection when finished.
def with_connection
connection_id = current_connection_id
- fresh_connection = true unless @reserved_connections[connection_id]
+ fresh_connection = true unless active_connection?
yield connection
ensure
release_connection(connection_id) if fresh_connection
@@ -123,122 +155,80 @@ module ActiveRecord
# Returns true if a connection has already been opened.
def connected?
- !@connections.empty?
+ synchronize { @connections.any? }
end
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
- @reserved_connections.each do |name,conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect!
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect!
+ end
+ @connections = []
end
- @connections = []
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- @reserved_connections.each do |name, conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect! if conn.requires_reloading?
- end
- @connections.delete_if do |conn|
- conn.requires_reloading?
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections.delete_if do |conn|
+ conn.requires_reloading?
+ end
end
end
# Verify active connections and remove and disconnect connections
# associated with stale threads.
def verify_active_connections! #:nodoc:
- clear_stale_cached_connections!
- @connections.each do |connection|
- connection.verify!
- end
- end
-
- def columns
- with_connection do |c|
- c.schema_cache.columns
- end
- end
- deprecate :columns
-
- def columns_hash
- with_connection do |c|
- c.schema_cache.columns_hash
- end
- end
- deprecate :columns_hash
-
- def primary_keys
- with_connection do |c|
- c.schema_cache.primary_keys
+ synchronize do
+ @connections.each do |connection|
+ connection.verify!
+ end
end
end
- deprecate :primary_keys
- def clear_cache!
- with_connection do |c|
- c.schema_cache.clear!
- end
- end
- deprecate :clear_cache!
-
- # Return any checked-out connections back to the pool by threads that
- # are no longer alive.
- def clear_stale_cached_connections!
- keys = @reserved_connections.keys - Thread.list.find_all { |t|
- t.alive?
- }.map { |thread| thread.object_id }
- keys.each do |key|
- checkin @reserved_connections[key]
- @reserved_connections.delete(key)
- end
+ def clear_stale_cached_connections! # :nodoc:
end
+ deprecate :clear_stale_cached_connections!
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
- # This is done by either returning an existing connection, or by creating
- # a new connection. If the maximum number of connections for this pool has
- # already been reached, but the pool is empty (i.e. they're all being used),
- # then this method will wait until a thread has checked in a connection.
- # The wait time is bounded however: if no connection can be checked out
- # within the timeout specified for this pool, then a ConnectionTimeoutError
- # exception will be raised.
+ # This is done by either returning and leasing existing connection, or by
+ # creating a new connection and leasing it.
+ #
+ # If all connections are leased and the pool is at capacity (meaning the
+ # number of currently leased connections is greater than or equal to the
+ # size limit set), an ActiveRecord::PoolFullError exception will be raised.
#
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool
- # within the timeout period.
+ # - PoolFullError: no connection can be obtained from the pool.
def checkout
# Checkout an available connection
- @connection_mutex.synchronize do
- loop do
- conn = if @checked_out.size < @connections.size
- checkout_existing_connection
- elsif @connections.size < @size
- checkout_new_connection
- end
- return conn if conn
-
- @queue.wait(@timeout)
-
- if(@checked_out.size < @connections.size)
- next
- 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."
- end
- end
+ synchronize do
+ # Try to find a connection that hasn't been leased, and lease it
+ conn = connections.find { |c| c.lease }
+
+ # If all connections were leased, and we have room to expand,
+ # create a new connection and lease it.
+ if !conn && connections.size < size
+ conn = checkout_new_connection
+ conn.lease
+ end
+ if conn
+ checkout_and_verify conn
+ else
+ raise PoolFullError.new(size, timeout)
end
end
end
@@ -249,16 +239,39 @@ module ActiveRecord
# +conn+: an AbstractAdapter object, which was obtained by earlier by
# calling +checkout+ on this pool.
def checkin(conn)
- @connection_mutex.synchronize do
+ synchronize do
conn.run_callbacks :checkin do
- @checked_out.delete conn
- @queue.signal
+ conn.expire
end
end
end
- synchronize :clear_reloadable_connections!, :verify_active_connections!,
- :connected?, :disconnect!, :with => :@connection_mutex
+ # Remove a connection from the connection pool. The connection will
+ # remain open and active but will no longer be managed by this pool.
+ def remove(conn)
+ synchronize do
+ @connections.delete conn
+
+ # FIXME: we might want to store the key on the connection so that removing
+ # from the reserved hash will be a little easier.
+ thread_id = @reserved_connections.keys.find { |k|
+ @reserved_connections[k] == conn
+ }
+ @reserved_connections.delete thread_id if thread_id
+ end
+ end
+
+ # Removes dead connections from the pool. A dead connection can occur
+ # if a programmer forgets to close a connection at the end of a thread
+ # or a thread dies unexpectedly.
+ def reap
+ synchronize do
+ stale = Time.now - @timeout
+ connections.dup.each do |conn|
+ remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ end
+ end
+ end
private
@@ -274,22 +287,21 @@ module ActiveRecord
raise ConnectionNotEstablished unless @automatic_reconnect
c = new_connection
+ c.pool = self
@connections << c
- checkout_and_verify(c)
- end
-
- def checkout_existing_connection
- c = (@connections - @checked_out).first
- checkout_and_verify(c)
+ c
end
def checkout_and_verify(c)
c.run_callbacks :checkout do
c.verify!
- @checked_out << c
end
c
end
+
+ def active_connections
+ @connections.find_all { |c| c.in_use? }
+ end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
@@ -320,10 +332,12 @@ module ActiveRecord
def initialize(pools = {})
@connection_pools = pools
+ @class_to_pool = {}
end
def establish_connection(name, spec)
- @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
+ @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec)
+ @class_to_pool[name] = @connection_pools[spec]
end
# Returns true if there are any active connections among the connection
@@ -374,52 +388,24 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(klass)
- pool = @connection_pools.delete(klass.name)
+ pool = @class_to_pool.delete(klass.name)
return nil unless pool
+ @connection_pools.delete pool.spec
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
def retrieve_connection_pool(klass)
- pool = @connection_pools[klass.name]
+ pool = @class_to_pool[klass.name]
return pool if pool
- return nil if ActiveRecord::Base == klass
- retrieve_connection_pool klass.superclass
+ return nil if ActiveRecord::Model == klass
+ retrieve_connection_pool klass.active_record_super
end
end
class ConnectionManagement
- class Proxy # :nodoc:
- attr_reader :body, :testing
-
- def initialize(body, testing = false)
- @body = body
- @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
-
- def close
- body.close if body.respond_to?(:close)
-
- # Don't return connection (and perform implicit rollback) if
- # this request is a part of integration test
- ActiveRecord::Base.clear_active_connections! unless testing
- end
- end
-
def initialize(app)
@app = app
end
@@ -427,9 +413,12 @@ module ActiveRecord
def call(env)
testing = env.key?('rack.test')
- status, headers, body = @app.call(env)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ ActiveRecord::Base.clear_active_connections! unless testing
+ end
- [status, headers, Proxy.new(body, testing)]
+ response
rescue
ActiveRecord::Base.clear_active_connections! unless testing
raise
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
deleted file mode 100644
index 3d0f146fed..0000000000
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ /dev/null
@@ -1,161 +0,0 @@
-module ActiveRecord
- class Base
- class ConnectionSpecification #:nodoc:
- attr_reader :config, :adapter_method
- def initialize (config, adapter_method)
- @config, @adapter_method = config, adapter_method
- end
- end
-
- ##
- # :singleton-method:
- # The connection handler
- class_attribute :connection_handler, :instance_writer => false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
-
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- self.class.connection
- end
-
- # Establishes the connection to the database. Accepts a hash as input where
- # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
- # example for regular databases (MySQL, Postgresql, etc):
- #
- # ActiveRecord::Base.establish_connection(
- # :adapter => "mysql",
- # :host => "localhost",
- # :username => "myuser",
- # :password => "mypass",
- # :database => "somedatabase"
- # )
- #
- # Example for SQLite database:
- #
- # ActiveRecord::Base.establish_connection(
- # :adapter => "sqlite",
- # :database => "path/to/dbfile"
- # )
- #
- # Also accepts keys as strings (for parsing from YAML for example):
- #
- # ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "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 = ENV["DATABASE_URL"])
- case spec
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- establish_connection(Rails.env)
- when ConnectionSpecification
- self.connection_handler.establish_connection(name, spec)
- 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
- else
- spec = spec.symbolize_keys
- unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
-
- begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
- rescue LoadError => e
- raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
- end
-
- adapter_method = "#{spec[:adapter]}_connection"
- unless respond_to?(adapter_method)
- raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
- end
-
- remove_connection
- establish_connection(ConnectionSpecification.new(spec, adapter_method))
- 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
- # to any of the specific Active Records.
- def connection
- 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
- # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
- #
- # Please use only for reading.
- def connection_config
- connection_pool.spec.config
- end
-
- def connection_pool
- connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
- end
-
- def retrieve_connection
- connection_handler.retrieve_connection(self)
- end
-
- # Returns true if Active Record is connected.
- def connected?
- connection_handler.connected?(self)
- end
-
- def remove_connection(klass = self)
- connection_handler.remove_connection(klass)
- end
-
- def clear_active_connections!
- connection_handler.clear_active_connections!
- end
-
- delegate :clear_reloadable_connections!,
- :clear_all_connections!,:verify_active_connections!, :to => :connection_handler
- end
- end
-end
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 dc4a53034b..eb8cff9610 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -130,7 +130,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
@@ -341,7 +341,7 @@ module ActiveRecord
# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
- def rollback_transaction_records(rollback) #:nodoc
+ def rollback_transaction_records(rollback)
if rollback
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
@@ -361,7 +361,7 @@ module ActiveRecord
end
# Send a commit message to all records after they have been committed.
- def commit_transaction_records #:nodoc
+ def commit_transaction_records
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index f93c7cd74a..44ac37c498 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -71,7 +71,8 @@ module ActiveRecord
when Date, Time then quoted_date(value)
when Symbol then value.to_s
else
- YAML.dump(value)
+ to_type = column ? " to #{column.type}" : ""
+ raise TypeError, "can't cast #{value.class}#{to_type}"
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 6f135b56b5..132ca10f79 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -66,6 +66,7 @@ module ActiveRecord
def initialize(base)
@columns = []
+ @columns_hash = {}
@base = base
end
@@ -86,7 +87,7 @@ module ActiveRecord
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
- @columns.find {|column| column.name.to_s == name.to_s}
+ @columns_hash[name.to_s]
end
# Instantiates a new column for the table.
@@ -224,28 +225,31 @@ module ActiveRecord
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
def column(name, type, options = {})
- column = self[name] || ColumnDefinition.new(@base, name, type)
- if options[:limit]
- column.limit = options[:limit]
- elsif native[type.to_sym].is_a?(Hash)
- column.limit = native[type.to_sym][:limit]
+ name = name.to_s
+ type = type.to_sym
+
+ column = self[name] || new_column_definition(@base, name, type)
+
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
end
+
+ column.limit = limit
column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- @columns << column unless @columns.include? column
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
self
end
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- #
- column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
- end # end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ type = :'#{column_type}' # type = :string
+ column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
+ end # end
EOV
end
@@ -275,9 +279,16 @@ module ActiveRecord
end
private
- def native
- @base.native_database_types
- end
+ def new_column_definition(base, name, type)
+ definition = ColumnDefinition.new base, name, type
+ @columns << definition
+ @columns_hash[name] = definition
+ definition
+ end
+
+ def native
+ @base.native_database_types
+ end
end
# Represents an SQL table in an abstract way for updating a table.
@@ -453,13 +464,13 @@ module ActiveRecord
def #{column_type}(*args) # def string(*args)
options = args.extract_options! # options = args.extract_options!
column_names = args # column_names = args
- #
+ type = :'#{column_type}' # type = :string
column_names.each do |name| # column_names.each do |name|
- column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
+ column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
if options[:limit] # if options[:limit]
column.limit = options[:limit] # column.limit = options[:limit]
- elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
- column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
+ elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
+ column.limit = native[type][:limit] # column.limit = native[type][:limit]
end # end
column.precision = options[:precision] # column.precision = options[:precision]
column.scale = options[:scale] # column.scale = options[:scale]
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 faa42e2d19..0cac6d1391 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,5 +1,5 @@
-require 'active_support/core_ext/array/wrap'
require 'active_support/deprecation/reporting'
+require 'active_record/schema_migration'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -16,8 +16,6 @@ module ActiveRecord
table_name[0...table_alias_length].gsub(/\./, '_')
end
- # def tables(name = nil) end
-
# Checks to see if the table +table_name+ exists on the database.
#
# === Example
@@ -44,7 +42,7 @@ module ActiveRecord
# # Check an index with a custom name exists
# index_exists?(:suppliers, :company_id, :name => "idx_company_id"
def index_exists?(table_name, column_name, options = {})
- column_names = Array.wrap(column_name)
+ column_names = Array(column_name)
index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
if options[:unique]
indexes(table_name).any?{ |i| i.unique && i.name == index_name }
@@ -114,7 +112,7 @@ module ActiveRecord
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
# Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via the +set_primary_key+ macro.
+ # need to configure the primary key in the model via +self.primary_key=+.
# Models do NOT auto-detect the primary key from their table definition.
#
# [<tt>:options</tt>]
@@ -303,15 +301,8 @@ module ActiveRecord
# 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,
- # unless you pass <tt>:name</tt> as an option.
- #
- # When creating an index on multiple columns, the first column is used as a name
- # for the index. For example, when you specify an index on two columns
- # [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an
- # index for the first column <tt>:first</tt>. Using just the first name for this index
- # makes sense, because you will never have to create a singular index with this
- # name.
+ # The index will be named after the table and the column name(s), unless
+ # you pass <tt>:name</tt> as an option.
#
# ===== Examples
#
@@ -386,7 +377,7 @@ module ActiveRecord
def index_name(table_name, options) #:nodoc:
if Hash === options # legacy support
if options[:column]
- "index_#{table_name}_on_#{Array.wrap(options[:column]) * '_and_'}"
+ "index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
elsif options[:name]
options[:name]
else
@@ -414,38 +405,20 @@ module ActiveRecord
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
- migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version")
- migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n")
+
+ ActiveRecord::SchemaMigration.order('version').all.map { |sm|
+ "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');"
+ }.join "\n\n"
end
# Should not be called normally, but this operation is non-destructive.
# The migrations module handles this automatically.
def initialize_schema_migrations_table
- sm_table = ActiveRecord::Migrator.schema_migrations_table_name
-
- unless table_exists?(sm_table)
- create_table(sm_table, :id => false) do |schema_migrations_table|
- schema_migrations_table.column :version, :string, :null => false
- end
- add_index sm_table, :version, :unique => true,
- :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
-
- # Backwards-compatibility: if we find schema_info, assume we've
- # migrated up to that point:
- si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
-
- if table_exists?(si_table)
- ActiveRecord::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table"
-
- old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
- assume_migrated_upto_version(old_version)
- drop_table(si_table)
- end
- end
+ ActiveRecord::SchemaMigration.create_table
end
def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
- migrations_paths = Array.wrap(migrations_paths)
+ migrations_paths = Array(migrations_paths)
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
@@ -560,7 +533,7 @@ module ActiveRecord
end
def add_index_options(table_name, column_name, options = {})
- column_names = Array.wrap(column_name)
+ column_names = Array(column_name)
index_name = index_name(table_name, :column => column_names)
if Hash === options # legacy support, since this param was a string
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 75e568b557..edea414db7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,12 +4,14 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_support/deprecation'
require 'active_record/connection_adapters/schema_cache'
+require 'monitor'
module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
autoload :Column
+ autoload :ConnectionSpecification
autoload_under 'abstract' do
autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
@@ -25,7 +27,6 @@ module ActiveRecord
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
@@ -48,21 +49,42 @@ module ActiveRecord
include DatabaseLimits
include QueryCache
include ActiveSupport::Callbacks
+ include MonitorMixin
define_callbacks :checkout, :checkin
- attr_accessor :visitor
- attr_reader :schema_cache
-
- def initialize(connection, logger = nil) #:nodoc:
- @active = nil
- @connection, @logger = connection, logger
+ attr_accessor :visitor, :pool
+ attr_reader :schema_cache, :last_use, :in_use
+ alias :in_use? :in_use
+
+ def initialize(connection, logger = nil, pool = nil) #:nodoc:
+ super()
+
+ @active = nil
+ @connection = connection
+ @in_use = false
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ @last_use = false
+ @logger = logger
+ @open_transactions = 0
+ @pool = pool
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
@query_cache_enabled = false
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @open_transactions = 0
- @instrumenter = ActiveSupport::Notifications.instrumenter
- @visitor = nil
- @schema_cache = SchemaCache.new self
+ @schema_cache = SchemaCache.new self
+ @visitor = nil
+ end
+
+ def lease
+ synchronize do
+ unless in_use
+ @in_use = true
+ @last_use = Time.now
+ end
+ end
+ end
+
+ def expire
+ @in_use = false
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -120,6 +142,12 @@ module ActiveRecord
false
end
+ # Does this adapter support explain? As of this writing sqlite3,
+ # mysql2, and postgresql are the only ones that do.
+ def supports_explain?
+ false
+ end
+
# QUOTING ==================================================
# Override to return the quoted table name. Defaults to column quoting.
@@ -236,6 +264,11 @@ module ActiveRecord
"active_record_#{open_transactions}"
end
+ # Check the connection back in to the connection pool
+ def close
+ pool.checkin self
+ end
+
protected
def log(sql, name = "SQL", binds = [])
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index f143fd348e..560773ca86 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -152,7 +152,7 @@ module ActiveRecord
true
end
- # Technically MySQL allows to create indexes with the sort order syntax
+ # Technically MySQL allows to create indexes with the sort order syntax
# but at the moment (5.5) it doesn't yet implement them
def supports_index_sort_order?
true
@@ -225,80 +225,6 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
- def explain(arel)
- sql = "EXPLAIN #{to_sql(arel)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN')
- elapsed = Time.now - start
-
- ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # MySQL shell:
- #
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
- # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # 2 rows in set (0.00 sec)
- #
- # This is an exercise in Ruby hyperrealism :).
- def pp(result, elapsed)
- widths = compute_column_widths(result)
- separator = build_separator(widths)
-
- pp = []
-
- pp << separator
- pp << build_cells(result.columns, widths)
- pp << separator
-
- result.rows.each do |row|
- pp << build_cells(row, widths)
- end
-
- pp << separator
- pp << build_footer(result.rows.length, elapsed)
-
- pp.join("\n") + "\n"
- end
-
- private
-
- def compute_column_widths(result)
- [].tap do |widths|
- result.columns.each_with_index do |column, i|
- cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
- widths << cells_in_column.map(&:length).max
- end
- end
- end
-
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- end
-
- def build_cells(items, widths)
- cells = []
- items.each_with_index do |item, i|
- item = 'NULL' if item.nil?
- justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
- cells << item.to_s.send(justifier, widths[i])
- end
- '| ' + cells.join(' | ') + ' |'
- end
-
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
- end
- end
-
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if name == :skip_logging
@@ -437,8 +363,10 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil) #:nodoc:
- sql = ["SHOW TABLES", database].compact.join(' IN ')
+ def tables(name = nil, database = nil, like = nil) #:nodoc:
+ sql = "SHOW TABLES "
+ sql << "IN #{database} " if database
+ sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
result.collect { |field| field.first }
@@ -446,7 +374,8 @@ module ActiveRecord
end
def table_exists?(name)
- return true if super
+ return false unless name
+ return true if tables(nil, nil, name).any?
name = name.to_s
schema, table = name.split('.', 2)
@@ -456,7 +385,7 @@ module ActiveRecord
schema = nil
end
- tables(nil, schema).include? table
+ tables(nil, schema, table).any?
end
# Returns an array of indexes for the given table.
@@ -573,9 +502,14 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("SHOW INDEX FROM #{quote_table_name(table)} WHERE Key_name = 'PRIMARY'", 'SCHEMA') do |result|
- keys = each_hash(result).map { |row| row[:Column_name] }
- keys.length == 1 ? [keys.first, nil] : nil
+ execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
+ create_table = each_hash(result).first[:"Create Table"]
+ if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/
+ keys = $1.split(",").map { |key| key.gsub(/`/, "") }
+ keys.length == 1 ? [keys.first, nil] : nil
+ else
+ nil
+ end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index a7856539b7..2ecb198edb 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,8 +5,8 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
module Format
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
@@ -80,9 +80,10 @@ module ActiveRecord
when :decimal then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
when :time then klass.string_to_dummy_time(value)
- when :date then klass.string_to_date(value)
+ when :date then klass.value_to_date(value)
when :binary then klass.binary_to_string(value)
when :boolean then klass.value_to_boolean(value)
+ when :hstore then klass.cast_hstore(value)
else value
end
end
@@ -97,9 +98,10 @@ module ActiveRecord
when :decimal then "#{klass}.value_to_decimal(#{var_name})"
when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.string_to_date(#{var_name})"
+ when :date then "#{klass}.value_to_date(#{var_name})"
when :binary then "#{klass}.binary_to_string(#{var_name})"
when :boolean then "#{klass}.value_to_boolean(#{var_name})"
+ when :hstore then "#{klass}.cast_hstore(#{var_name})"
else var_name
end
end
@@ -132,11 +134,15 @@ module ActiveRecord
value
end
- def string_to_date(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_date(string) || fallback_string_to_date(string)
+ def value_to_date(value)
+ if value.is_a?(String)
+ return nil if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
end
def string_to_time(string)
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
new file mode 100644
index 0000000000..8491d42b86
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -0,0 +1,83 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+
+ def initialize(config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+
+ def initialize_dup(original)
+ @config = original.config.dup
+ end
+
+ ##
+ # Builds a ConnectionSpecification from user input
+ class Resolver # :nodoc:
+ attr_reader :config, :klass, :configurations
+
+ def initialize(config, configurations)
+ @config = config
+ @configurations = configurations
+ end
+
+ def spec
+ case config
+ when nil
+ raise AdapterNotSpecified unless defined?(Rails.env)
+ resolve_string_connection Rails.env
+ when Symbol, String
+ resolve_string_connection config.to_s
+ when Hash
+ resolve_hash_connection config
+ end
+ end
+
+ private
+ def resolve_string_connection(spec) # :nodoc:
+ hash = configurations.fetch(spec) do |k|
+ connection_url_to_hash(k)
+ end
+
+ raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
+
+ resolve_hash_connection hash
+ end
+
+ def resolve_hash_connection(spec) # :nodoc:
+ spec = spec.symbolize_keys
+
+ raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+
+ begin
+ require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ rescue LoadError => e
+ raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+
+ ConnectionSpecification.new(spec, adapter_method)
+ end
+
+ def 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
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 971f3c35f3..6086c32dbe 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.6'
+gem 'mysql2', '~> 0.3.10'
require 'mysql2'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql2_connection(config)
+ def mysql2_connection(config)
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
@@ -35,6 +35,10 @@ module ActiveRecord
configure_connection
end
+ def supports_explain?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -93,6 +97,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
# The overrides below perform much better than the originals in AbstractAdapter
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index f092edecda..e432c5af32 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -18,9 +18,9 @@ class Mysql
end
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql_connection(config) # :nodoc:
+ def mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -224,52 +224,48 @@ module ActiveRecord
@statements.clear
end
- if "<3".respond_to?(:encode)
- # Taken from here:
- # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
- # Author: TOMITA Masahiro <tommy@tmtm.org>
- ENCODINGS = {
- "armscii8" => nil,
- "ascii" => Encoding::US_ASCII,
- "big5" => Encoding::Big5,
- "binary" => Encoding::ASCII_8BIT,
- "cp1250" => Encoding::Windows_1250,
- "cp1251" => Encoding::Windows_1251,
- "cp1256" => Encoding::Windows_1256,
- "cp1257" => Encoding::Windows_1257,
- "cp850" => Encoding::CP850,
- "cp852" => Encoding::CP852,
- "cp866" => Encoding::IBM866,
- "cp932" => Encoding::Windows_31J,
- "dec8" => nil,
- "eucjpms" => Encoding::EucJP_ms,
- "euckr" => Encoding::EUC_KR,
- "gb2312" => Encoding::EUC_CN,
- "gbk" => Encoding::GBK,
- "geostd8" => nil,
- "greek" => Encoding::ISO_8859_7,
- "hebrew" => Encoding::ISO_8859_8,
- "hp8" => nil,
- "keybcs2" => nil,
- "koi8r" => Encoding::KOI8_R,
- "koi8u" => Encoding::KOI8_U,
- "latin1" => Encoding::ISO_8859_1,
- "latin2" => Encoding::ISO_8859_2,
- "latin5" => Encoding::ISO_8859_9,
- "latin7" => Encoding::ISO_8859_13,
- "macce" => Encoding::MacCentEuro,
- "macroman" => Encoding::MacRoman,
- "sjis" => Encoding::SHIFT_JIS,
- "swe7" => nil,
- "tis620" => Encoding::TIS_620,
- "ucs2" => Encoding::UTF_16BE,
- "ujis" => Encoding::EucJP_ms,
- "utf8" => Encoding::UTF_8,
- "utf8mb4" => Encoding::UTF_8,
- }
- else
- ENCODINGS = Hash.new { |h,k| h[k] = k }
- end
+ # Taken from here:
+ # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
+ # Author: TOMITA Masahiro <tommy@tmtm.org>
+ ENCODINGS = {
+ "armscii8" => nil,
+ "ascii" => Encoding::US_ASCII,
+ "big5" => Encoding::Big5,
+ "binary" => Encoding::ASCII_8BIT,
+ "cp1250" => Encoding::Windows_1250,
+ "cp1251" => Encoding::Windows_1251,
+ "cp1256" => Encoding::Windows_1256,
+ "cp1257" => Encoding::Windows_1257,
+ "cp850" => Encoding::CP850,
+ "cp852" => Encoding::CP852,
+ "cp866" => Encoding::IBM866,
+ "cp932" => Encoding::Windows_31J,
+ "dec8" => nil,
+ "eucjpms" => Encoding::EucJP_ms,
+ "euckr" => Encoding::EUC_KR,
+ "gb2312" => Encoding::EUC_CN,
+ "gbk" => Encoding::GBK,
+ "geostd8" => nil,
+ "greek" => Encoding::ISO_8859_7,
+ "hebrew" => Encoding::ISO_8859_8,
+ "hp8" => nil,
+ "keybcs2" => nil,
+ "koi8r" => Encoding::KOI8_R,
+ "koi8u" => Encoding::KOI8_U,
+ "latin1" => Encoding::ISO_8859_1,
+ "latin2" => Encoding::ISO_8859_2,
+ "latin5" => Encoding::ISO_8859_9,
+ "latin7" => Encoding::ISO_8859_13,
+ "macce" => Encoding::MacCentEuro,
+ "macroman" => Encoding::MacRoman,
+ "sjis" => Encoding::SHIFT_JIS,
+ "swe7" => nil,
+ "tis620" => Encoding::TIS_620,
+ "ucs2" => Encoding::UTF_16BE,
+ "ujis" => Encoding::EucJP_ms,
+ "utf8" => Encoding::UTF_8,
+ "utf8mb4" => Encoding::UTF_8,
+ }
# Get the client encoding for this database
def client_encoding
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 2f01fbb829..dbfb375ba8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -7,24 +7,26 @@ gem 'pg', '~> 0.11'
require 'pg'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects
- def self.postgresql_connection(config) # :nodoc:
- config = config.symbolize_keys
- host = config[:host]
- port = config[:port] || 5432
- username = config[:username].to_s if config[:username]
- password = config[:password].to_s if config[:password]
+ def postgresql_connection(config) # :nodoc:
+ conn_params = config.symbolize_keys
- if config.key?(:database)
- database = config[:database]
- else
- raise ArgumentError, "No database specified. Missing argument: database."
+ # Forward any unused config params to PGconn.connect.
+ [:statement_limit, :encoding, :min_messages, :schema_search_path,
+ :schema_order, :adapter, :pool, :wait_timeout,
+ :reaping_frequency].each do |key|
+ conn_params.delete key
end
+ conn_params.delete_if { |k,v| v.nil? }
+
+ # Map ActiveRecords param names to PGs.
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
- ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
+ ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
end
end
@@ -49,6 +51,42 @@ module ActiveRecord
super
end
end
+
+ def cast_hstore(object)
+ if Hash === object
+ object.map { |k,v|
+ "#{escape_hstore(k)}=>#{escape_hstore(v)}"
+ }.join ', '
+ else
+ kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o|
+ unescape_hstore(o[1...-1])
+ }
+ Hash[kvs.each_slice(2).to_a]
+ end
+ end
+
+ private
+ HSTORE_ESCAPE = {
+ ' ' => '\\ ',
+ '\\' => '\\\\',
+ '"' => '\\"',
+ '=' => '\\=',
+ }
+ HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys)
+ HSTORE_UNESCAPE = HSTORE_ESCAPE.invert
+ HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys)
+
+ def unescape_hstore(value)
+ value.gsub(HSTORE_UNESCAPE_RE) do |match|
+ HSTORE_UNESCAPE[match]
+ end
+ end
+
+ def escape_hstore(value)
+ value.gsub(HSTORE_ESCAPE_RE) do |match|
+ HSTORE_ESCAPE[match]
+ end
+ end
end
# :startdoc:
@@ -79,67 +117,69 @@ module ActiveRecord
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when 'interval'
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Network address types
- when /^(?:cidr|inet|macaddr)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :string
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
+ # Numeric and monetary types
+ when /^(?:real|double precision)$/
+ :float
+ # Monetary types
+ when 'money'
+ :decimal
+ when 'hstore'
+ :hstore
+ # Character types
+ when /^(?:character varying|bpchar)(?:\(\d+\))?$/
+ :string
+ # Binary data types
+ when 'bytea'
+ :binary
+ # Date/time types
+ when /^timestamp with(?:out)? time zone$/
+ :datetime
+ when 'interval'
+ :string
+ # Geometric types
+ when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
+ :string
+ # Network address types
+ when /^(?:cidr|inet|macaddr)$/
+ :string
+ # Bit strings
+ when /^bit(?: varying)?(?:\(\d+\))?$/
+ :string
+ # XML type
+ when 'xml'
+ :xml
+ # tsvector type
+ when 'tsvector'
+ :tsvector
+ # Arrays
+ when /^\D+\[\]$/
+ :string
+ # Object identifier types
+ when 'oid'
+ :integer
+ # UUID type
+ when 'uuid'
+ :string
+ # Small and big integer types
+ when /^(?:small|big)int$/
+ :integer
+ # Pass through all types that are not specific to PostgreSQL.
+ else
+ super
end
end
# Extracts the value from a PostgreSQL column default definition.
def self.extract_value_from_default(default)
+ # This is a performance optimization for Ruby 1.9.2 in development.
+ # If the value is nil, we return nil straight away without checking
+ # the regular expressions. If we check each regular expression,
+ # Regexp#=== will call NilClass#to_str, which will trigger
+ # method_missing (defined by whiny nil in ActiveSupport) which
+ # makes this method very very slow.
+ return default unless default
+
case default
- # This is a performance optimization for Ruby 1.9.2 in development.
- # If the value is nil, we return nil straight away without checking
- # the regular expressions. If we check each regular expression,
- # Regexp#=== will call NilClass#to_str, which will trigger
- # method_missing (defined by whiny nil in ActiveSupport) which
- # makes this method very very slow.
- when NilClass
- nil
# Numeric types
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
$1
@@ -188,22 +228,29 @@ module ActiveRecord
end
end
- # The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
- # Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
+ # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
#
# Options:
#
- # * <tt>:host</tt> - Defaults to "localhost".
+ # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets,
+ # the default is to connect to localhost.
# * <tt>:port</tt> - Defaults to 5432.
- # * <tt>:username</tt> - Defaults to nothing.
- # * <tt>:password</tt> - Defaults to nothing.
- # * <tt>:database</tt> - The name of the database. No default, must be provided.
+ # * <tt>:username</tt> - Defaults to be the same as the operating system name of the user running the application.
+ # * <tt>:password</tt> - Password to be used if the server demands password authentication.
+ # * <tt>:database</tt> - Defaults to be the same as the user name.
# * <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.
# * <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
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
+ #
+ # Any further options are used as connection parameters to libpq. See
+ # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
+ # list of parameters.
+ #
+ # In addition, default connection parameters of libpq can be set per environment variables.
+ # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
def xml(*args)
@@ -215,6 +262,10 @@ module ActiveRecord
options = args.extract_options!
column(args[0], 'tsvector', options)
end
+
+ def hstore(name, options = {})
+ column(name, 'hstore', options)
+ end
end
ADAPTER_NAME = 'PostgreSQL'
@@ -390,6 +441,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
@table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
@@ -514,9 +570,9 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
- def explain(arel)
+ def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
end
class ExplainPrettyPrinter # :nodoc:
@@ -829,7 +885,7 @@ module ActiveRecord
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
-
+
column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
end.compact
end
@@ -1153,7 +1209,7 @@ module ActiveRecord
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
- @connection = PGconn.connect(*@connection_parameters)
+ @connection = PGconn.connect(@connection_parameters)
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index b14b37ce89..4e8932a695 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -2,22 +2,14 @@ module ActiveRecord
module ConnectionAdapters
class SchemaCache
attr_reader :columns, :columns_hash, :primary_keys, :tables
- attr_reader :column_defaults
attr_reader :connection
def initialize(conn)
@connection = conn
- @tables = {}
+ @tables = {}
- @columns = Hash.new do |h, table_name|
- h[table_name] =
- # Fetch a list of columns
- conn.columns(table_name, "#{table_name} Columns").tap do |cs|
- # set primary key information
- cs.each do |column|
- column.primary = column.name == primary_keys[table_name]
- end
- end
+ @columns = Hash.new do |h, table_name|
+ h[table_name] = conn.columns(table_name, "#{table_name} Columns")
end
@columns_hash = Hash.new do |h, table_name|
@@ -26,15 +18,8 @@ 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] = table_exists?(table_name) ?
- conn.primary_key(table_name) : 'id'
+ h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
end
end
@@ -42,21 +27,14 @@ module ActiveRecord
def table_exists?(name)
return @tables[name] if @tables.key? name
- connection.tables.each { |table| @tables[table] = true }
- @tables[name] = connection.table_exists?(name) if !@tables.key?(name)
-
- @tables[name]
+ @tables[name] = connection.table_exists?(name)
end
- # Clears out internal caches:
- #
- # * columns
- # * columns_hash
- # * tables
+ # Clears out internal caches
def clear!
@columns.clear
@columns_hash.clear
- @column_defaults.clear
+ @primary_keys.clear
@tables.clear
end
@@ -64,8 +42,8 @@ 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
+ @tables.delete table_name
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 0a0da0b5d3..ee5d10859c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/sqlite_adapter'
-gem 'sqlite3', '~> 1.3.4'
+gem 'sqlite3', '~> 1.3.5'
require 'sqlite3'
module ActiveRecord
- class Base
+ module ConnectionHandling
# sqlite3 adapter reuses sqlite_connection.
- def self.sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config) # :nodoc:
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -47,11 +47,7 @@ module ActiveRecord
# Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
- if @connection.respond_to?(:encoding)
- @connection.encoding.to_s
- else
- @connection.execute('PRAGMA encoding')[0]['encoding']
- end
+ @connection.encoding.to_s
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index c11f82a33f..69750a911d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -16,7 +16,7 @@ module ActiveRecord
end
def binary_to_string(value)
- if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
+ if value.encoding != Encoding::ASCII_8BIT
value = value.force_encoding(Encoding::ASCII_8BIT)
end
@@ -122,6 +122,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
def requires_reloading?
true
end
@@ -196,32 +201,24 @@ 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
+ 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
+ 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
# DATABASE STATEMENTS ======================================
- def explain(arel)
+ def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN'))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
end
class ExplainPrettyPrinter
@@ -324,18 +321,23 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def tables(name = 'SCHEMA') #:nodoc:
+ def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
sql = <<-SQL
SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
+ sql << " AND name = #{quote_table_name(table_name)}" if table_name
exec_query(sql, name).map do |row|
row['name']
end
end
+ def table_exists?(name)
+ name && tables('SCHEMA', name).any?
+ end
+
# Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map do |field|
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
new file mode 100644
index 0000000000..d7746826a9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -0,0 +1,96 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module ConnectionHandling
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :database => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from YAML for example):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "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 establish_connection(spec = ENV["DATABASE_URL"])
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
+ spec = resolver.spec
+
+ unless respond_to?(spec.adapter_method)
+ raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
+ end
+
+ remove_connection
+ connection_handler.establish_connection name, spec
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def connection
+ 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
+ # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ #
+ # Please use only for reading.
+ def connection_config
+ connection_pool.spec.config
+ end
+
+ def connection_pool
+ connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
+ end
+
+ def retrieve_connection
+ connection_handler.retrieve_connection(self)
+ end
+
+ # Returns true if Active Record is connected.
+ def connected?
+ connection_handler.connected?(self)
+ end
+
+ def remove_connection(klass = self)
+ connection_handler.remove_connection(klass)
+ end
+
+ delegate :clear_active_connections!, :clear_reloadable_connections!,
+ :clear_all_connections!, :verify_active_connections!, :to => :connection_handler
+ end
+end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
new file mode 100644
index 0000000000..adba3f710c
--- /dev/null
+++ b/activerecord/lib/active_record/core.rb
@@ -0,0 +1,341 @@
+require 'active_support/concern'
+require 'thread'
+
+module ActiveRecord
+ module Core
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
+ # which is then passed on to any new database connections made and which can be retrieved on both
+ # a class and instance level by calling +logger+.
+ config_attribute :logger, :global => true
+
+ ##
+ # :singleton-method:
+ # Contains the database configuration - as is typically stored in config/database.yml -
+ # as a Hash.
+ #
+ # For example, the following database.yml...
+ #
+ # development:
+ # adapter: sqlite3
+ # database: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # {
+ # 'development' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/development.sqlite3'
+ # },
+ # 'production' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/production.sqlite3'
+ # }
+ # }
+ config_attribute :configurations, :global => true
+ self.configurations = {}
+
+ ##
+ # :singleton-method:
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
+ # dates and times from the database. This is set to :utc by default.
+ config_attribute :default_timezone, :global => true
+ self.default_timezone = :utc
+
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ config_attribute :schema_format, :global => true
+ self.schema_format = :ruby
+
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ config_attribute :timestamped_migrations, :global => true
+ self.timestamped_migrations = true
+
+ ##
+ # :singleton-method:
+ # The connection handler
+ config_attribute :connection_handler
+ self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ end
+
+ module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules
+ @attribute_methods_mutex = Mutex.new
+
+ # force attribute methods to be higher in inheritance hierarchy than other generated methods
+ generated_attribute_methods
+ generated_feature_methods
+ end
+
+ def generated_feature_methods
+ @generated_feature_methods ||= begin
+ mod = const_set(:GeneratedFeatureMethods, Module.new)
+ include mod
+ mod
+ end
+ end
+
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
+ def inspect
+ if self == Base
+ super
+ elsif abstract_class?
+ "#{super}(abstract)"
+ elsif table_exists?
+ attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
+ "#{super}(#{attr_list})"
+ else
+ "#{super}(Table doesn't exist)"
+ end
+ end
+
+ # Overwrite the default class equality method to provide support for association proxies.
+ def ===(object)
+ object.is_a?(self)
+ end
+
+ def arel_table
+ @arel_table ||= Arel::Table.new(table_name, arel_engine)
+ end
+
+ def arel_engine
+ @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine
+ end
+
+ private
+
+ def relation #:nodoc:
+ @relation ||= Relation.new(self, arel_table)
+
+ if finder_needs_type_condition?
+ @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ else
+ @relation
+ end
+ end
+ end
+
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
+ # hence you can't have attributes that aren't part of the table columns.
+ #
+ # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
+ # in the +options+ parameter.
+ #
+ # ==== Examples
+ # # Instantiates a single new object
+ # User.new(:first_name => 'Jamie')
+ #
+ # # Instantiates a single new object using the :admin mass-assignment security role
+ # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
+ #
+ # # Instantiates a single new object bypassing mass-assignment security
+ # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
+ def initialize(attributes = nil, options = {})
+ @attributes = self.class.initialize_attributes(self.class.column_defaults.dup)
+
+ init_internals
+
+ ensure_proper_type
+
+ populate_with_current_scope_attributes
+
+ assign_attributes(attributes, options) if attributes
+
+ yield self if block_given?
+ run_callbacks :initialize
+ end
+
+ # Initialize an empty model object from +coder+. +coder+ must contain
+ # the attributes necessary for initializing an empty model object. For
+ # example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ #
+ # post = Post.allocate
+ # post.init_with('attributes' => { 'title' => 'hello world' })
+ # post.title # => 'hello world'
+ def init_with(coder)
+ @attributes = self.class.initialize_attributes(coder['attributes'])
+
+ init_internals
+
+ @new_record = false
+
+ run_callbacks :find
+ run_callbacks :initialize
+
+ self
+ end
+
+ # Duped objects have no id assigned and are treated as new records. Note
+ # that this is a "shallow" copy as it copies the object's attributes
+ # only, not its associations. The extent of a "deep" copy is application
+ # specific and is therefore left to the application to implement according
+ # to its need.
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
+ def initialize_dup(other)
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
+ cloned_attributes.delete(self.class.primary_key)
+
+ @attributes = cloned_attributes
+
+ run_callbacks(:initialize) if _initialize_callbacks.any?
+
+ @changed_attributes = {}
+ self.class.column_defaults.each do |attr, orig_value|
+ @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
+ end
+
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+
+ @new_record = true
+
+ ensure_proper_type
+ populate_with_current_scope_attributes
+ super
+ end
+
+ # Populate +coder+ with attributes about this record that should be
+ # serialized. The structure of +coder+ defined in this method is
+ # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # method.
+ #
+ # Example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ # coder = {}
+ # Post.new.encode_with(coder)
+ # coder # => { 'id' => nil, ... }
+ def encode_with(coder)
+ coder['attributes'] = attributes
+ end
+
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
+ #
+ # Note that new records are different from any other record by definition, unless the
+ # other record is the receiver itself. Besides, if you fetch existing records with
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
+ #
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
+ # models are still comparable.
+ def ==(comparison_object)
+ super ||
+ comparison_object.instance_of?(self.class) &&
+ id.present? &&
+ comparison_object.id == id
+ 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) ]
+ def hash
+ id.hash
+ end
+
+ # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ def freeze
+ @attributes.freeze; self
+ end
+
+ # Returns +true+ if the attributes hash has been frozen.
+ def frozen?
+ @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
+
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
+ # attributes will be marked as read only since they cannot be saved.
+ def readonly?
+ @readonly
+ end
+
+ # Marks this record as read only.
+ def readonly!
+ @readonly = true
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work that isn't
+ # easily done without going straight to SQL.
+ def connection
+ self.class.connection
+ end
+
+ # Returns the contents of the record as a nicely formatted string.
+ def inspect
+ 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
+
+ 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 init_internals
+ @relation = nil
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @previously_changed = {}
+ @changed_attributes = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @new_record = true
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 3c7defedac..224f5276eb 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -19,15 +19,10 @@ module ActiveRecord
counters.each do |association|
has_many_association = reflect_on_association(association.to_sym)
- expected_name = if has_many_association.options[:as]
- has_many_association.options[:as].to_s.classify
- else
- self.name
- end
-
+ foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.class_name == expected_name }
+ reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key }
counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
@@ -65,7 +60,7 @@ module ActiveRecord
# Post.update_counters [10, 15], :comment_count => 1
# # Executes the following SQL:
# # UPDATE posts
- # # SET comment_count = COALESCE(comment_count, 0) + 1,
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
# # WHERE id IN (10, 15)
def update_counters(id, counters)
updates = counters.map do |counter_name, value|
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
new file mode 100644
index 0000000000..e9068089f0
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -0,0 +1,79 @@
+module ActiveRecord
+ module DynamicMatchers
+ def respond_to?(method_id, include_private = false)
+ if match = DynamicFinderMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
+ end
+
+ super
+ end
+
+ private
+
+ # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
+ # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
+ # section at the top of this file for more detailed information.
+ #
+ # It's even possible to use all the additional parameters to +find+. For example, the
+ # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
+ #
+ # 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) || DynamicScopeMatch.match(method_id))
+ attribute_names = match.attribute_names
+ super unless all_attributes_exists?(attribute_names)
+ 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
+ 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)]
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ 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
+ end
+ end
+
+ # Similar in purpose to +expand_hash_conditions_for_aggregates+.
+ def expand_attribute_names_for_aggregates(attribute_names)
+ attribute_names.map { |attribute_name|
+ unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
+ aggregate_mapping(aggregation).map do |field_attr, _|
+ field_attr.to_sym
+ end
+ else
+ attribute_name.to_sym
+ end
+ }.flatten
+ end
+
+ def all_attributes_exists?(attribute_names)
+ (expand_attribute_names_for_aggregates(attribute_names) -
+ column_methods_hash.keys).empty?
+ end
+
+ def aggregate_mapping(reflection)
+ mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
+ mapping.first.is_a?(Array) ? mapping : [mapping]
+ end
+
+
+ end
+end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
new file mode 100644
index 0000000000..e502d7e52b
--- /dev/null
+++ b/activerecord/lib/active_record/explain.rb
@@ -0,0 +1,80 @@
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ module Explain
+ def self.extended(base)
+ # If a query takes longer than these many seconds we log its query plan
+ # automatically. nil disables this feature.
+ base.config_attribute :auto_explain_threshold_in_seconds, :global => true
+ end
+
+ # If auto explain is enabled, this method triggers EXPLAIN logging for the
+ # queries triggered by the block if it takes more than the threshold as a
+ # whole. That is, the threshold is not checked against each individual
+ # query, but against the duration of the entire block. This approach is
+ # convenient for relations.
+ #
+ # The available_queries_for_explain thread variable collects the queries
+ # to be explained. If the value is nil, it means queries are not being
+ # currently collected. A false value indicates collecting is turned
+ # off. Otherwise it is an array of queries.
+ def logging_query_plan # :nodoc:
+ threshold = auto_explain_threshold_in_seconds
+ current = Thread.current
+ if threshold && current[:available_queries_for_explain].nil?
+ begin
+ queries = current[:available_queries_for_explain] = []
+ start = Time.now
+ result = yield
+ logger.warn(exec_explain(queries)) if Time.now - start > threshold
+ result
+ ensure
+ current[:available_queries_for_explain] = nil
+ end
+ else
+ yield
+ end
+ end
+
+ # Relation#explain needs to be able to collect the queries regardless of
+ # whether auto explain is enabled. This method serves that purpose.
+ def collecting_queries_for_explain # :nodoc:
+ current = Thread.current
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
+ return yield, current[:available_queries_for_explain]
+ ensure
+ # Note that the return value above does not depend on this assigment.
+ current[:available_queries_for_explain] = original
+ end
+
+ # Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
+ # Returns a formatted string ready to be logged.
+ def exec_explain(queries) # :nodoc:
+ queries && queries.map do |sql, bind|
+ [].tap do |msg|
+ msg << "EXPLAIN for: #{sql}"
+ unless bind.empty?
+ bind_msg = bind.map {|col, val| [col.name, val]}.inspect
+ msg.last << " #{bind_msg}"
+ end
+ msg << connection.explain(sql, bind)
+ end.join("\n")
+ end.join("\n")
+ end
+
+ # Silences automatic EXPLAIN logging for the duration of the block.
+ #
+ # This has high priority, no EXPLAINs will be run even if downwards
+ # the threshold is set to 0.
+ #
+ # As the name of the method suggests this only applies to automatic
+ # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
+ def silence_auto_explain
+ current = Thread.current
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
+ yield
+ ensure
+ current[:available_queries_for_explain] = original
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
new file mode 100644
index 0000000000..fc76410499
--- /dev/null
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -0,0 +1,21 @@
+require 'active_support/notifications'
+
+module ActiveRecord
+ class ExplainSubscriber # :nodoc:
+ def call(*args)
+ if queries = Thread.current[:available_queries_for_explain]
+ payload = args.last
+ queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ end
+ end
+
+ # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
+ # our own EXPLAINs now matter how loopingly beautiful that would be.
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
+ def ignore_payload?(payload)
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name])
+ end
+
+ ActiveSupport::Notifications.subscribe("sql.active_record", new)
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index cad9417216..cf315b687c 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,16 +1,8 @@
require 'erb'
-
-begin
- require 'psych'
-rescue LoadError
-end
-
require 'yaml'
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_record/fixtures/file'
@@ -22,8 +14,6 @@ else
end
end
-class FixturesFileNotFound < StandardError; end
-
module ActiveRecord
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
@@ -391,10 +381,15 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(table_name) # :nodoc:
+ def self.default_fixture_model_name(fixture_name) # :nodoc:
ActiveRecord::Base.pluralize_table_names ?
- table_name.to_s.singularize.camelize :
- table_name.to_s.camelize
+ fixture_name.singularize.camelize :
+ fixture_name.camelize
+ end
+
+ def self.default_fixture_table_name(fixture_name) # :nodoc:
+ "#{ActiveRecord::Base.table_name_prefix}"\
+ "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym
end
def self.reset_cache
@@ -443,10 +438,8 @@ module ActiveRecord
self.all_loaded_fixtures = {}
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
- table_names = [table_names].flatten.map { |n| n.to_s }
- table_names.each { |n|
- class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/')
- }
+ table_names = Array(table_names).map(&:to_s)
+ class_names = class_names.stringify_keys
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -460,12 +453,12 @@ module ActiveRecord
fixtures_map = {}
fixture_files = files_to_read.map do |path|
- table_name = path.tr '/', '_'
+ fixture_name = path
- fixtures_map[path] = ActiveRecord::Fixtures.new(
+ fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new
connection,
- table_name,
- class_names[table_name.to_sym] || table_name.classify,
+ fixture_name,
+ class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name),
::File.join(fixtures_directory, path))
end
@@ -489,8 +482,8 @@ module ActiveRecord
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
- table_names.each do |table_name|
- connection.reset_pk_sequence!(table_name.tr('/', '_'))
+ fixture_files.each do |ff|
+ connection.reset_pk_sequence!(ff.table_name)
end
end
end
@@ -509,25 +502,27 @@ module ActiveRecord
attr_reader :table_name, :name, :fixtures, :model_class
- def initialize(connection, table_name, class_name, fixture_path)
+ def initialize(connection, fixture_name, class_name, fixture_path)
@connection = connection
- @table_name = table_name
@fixture_path = fixture_path
- @name = table_name # preserve fixture base name
+ @name = fixture_name
@class_name = class_name
@fixtures = ActiveSupport::OrderedHash.new
- @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
# Should be an AR::Base type class
if class_name.is_a?(Class)
- @table_name = class_name.table_name
- @connection = class_name.connection
- @model_class = class_name
+ @model_class = class_name
else
- @model_class = class_name.constantize rescue nil
+ @model_class = class_name.constantize rescue nil
end
+ @connection = model_class.connection if model_class && model_class.respond_to?(:connection)
+
+ @table_name = ( model_class.respond_to?(:table_name) ?
+ model_class.table_name :
+ self.class.default_fixture_table_name(fixture_name) )
+
read_fixture_files
end
@@ -562,7 +557,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class && model_class < ActiveRecord::Model
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |name|
@@ -644,14 +639,6 @@ module ActiveRecord
end
def read_fixture_files
- if ::File.file?(yaml_file_path)
- read_yaml_fixture_files
- else
- raise FixturesFileNotFound, "Could not find #{yaml_file_path}"
- end
- end
-
- def read_yaml_fixture_files
yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path]
@@ -734,20 +721,37 @@ module ActiveRecord
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
- self.fixture_class_names = Hash.new do |h, table_name|
- h[table_name] = ActiveRecord::Fixtures.find_table_name(table_name)
+ self.fixture_class_names = Hash.new do |h, fixture_name|
+ fixture_name = fixture_name.to_s
+ h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name)
end
end
module ClassMethods
+ # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
+ #
+ # Examples:
+ #
+ # set_fixture_class :some_fixture => SomeModel,
+ # 'namespaced/fixture' => Another::Model
+ #
+ # The keys must be the fixture names, that coincide with the short paths to the fixture files.
+ #--
+ # It is also possible to pass the class name instead of the class:
+ # set_fixture_class 'some_fixture' => 'SomeModel'
+ # I think this option is redundant, i propose to deprecate it.
+ # Isn't it easier to always pass the class itself?
+ # (2011-12-20 alexeymuranov)
+ #++
def set_fixture_class(class_names = {})
- self.fixture_class_names = self.fixture_class_names.merge(class_names)
+ self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_names)
if fixture_names.first == :all
- fixture_names = Dir["#{fixture_path}/**/*.{yml}"]
- fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
+ fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f|
+ File.basename f, '.yml'
+ }
else
fixture_names = fixture_names.flatten.map { |n| n.to_s }
end
@@ -778,12 +782,13 @@ module ActiveRecord
end
def setup_fixture_accessors(fixture_names = nil)
- fixture_names = Array.wrap(fixture_names || fixture_table_names)
+ fixture_names = Array(fixture_names || fixture_table_names)
methods = Module.new do
fixture_names.each do |fixture_name|
- fixture_name = fixture_name.to_s.tr('./', '_')
+ fixture_name = fixture_name.to_s
+ accessor_name = fixture_name.tr('/', '_').to_sym
- define_method(fixture_name) do |*fixtures|
+ define_method(accessor_name) do |*fixtures|
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
@fixture_cache[fixture_name] ||= {}
@@ -796,13 +801,13 @@ module ActiveRecord
@fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
end
else
- raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'"
+ raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
- private fixture_name
+ private accessor_name
end
end
include methods
@@ -832,6 +837,7 @@ module ActiveRecord
end
@fixture_cache = {}
+ @fixture_connections = []
@@already_loaded_fixtures ||= {}
# Load fixtures once and begin transaction.
diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb
index 6bad36abb9..6547791144 100644
--- a/activerecord/lib/active_record/fixtures/file.rb
+++ b/activerecord/lib/active_record/fixtures/file.rb
@@ -1,8 +1,3 @@
-begin
- require 'psych'
-rescue LoadError
-end
-
require 'erb'
require 'yaml'
diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb
index b15b5a8133..d9777bb2f6 100644
--- a/activerecord/lib/active_record/identity_map.rb
+++ b/activerecord/lib/active_record/identity_map.rb
@@ -111,37 +111,18 @@ module ActiveRecord
# model object.
def reinit_with(coder)
@attributes_cache = {}
- dirty = @changed_attributes.keys
- @attributes.update(coder['attributes'].except(*dirty))
+ dirty = @changed_attributes.keys
+ attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty))
+ @attributes.update(attributes)
@changed_attributes.update(coder['attributes'].slice(*dirty))
@changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
- set_serialized_attributes
-
run_callbacks :find
self
end
class Middleware
- class Body #:nodoc:
- def initialize(target, original)
- @target = target
- @original = original
- end
-
- def each(&block)
- @target.each(&block)
- end
-
- def close
- @target.close if @target.respond_to?(:close)
- ensure
- IdentityMap.enabled = @original
- IdentityMap.clear
- end
- end
-
def initialize(app)
@app = app
end
@@ -149,8 +130,14 @@ module ActiveRecord
def call(env)
enabled = IdentityMap.enabled
IdentityMap.enabled = true
- status, headers, body = @app.call(env)
- [status, headers, Body.new(body, enabled)]
+
+ response = @app.call(env)
+ response[2] = Rack::BodyProxy.new(response[2]) do
+ IdentityMap.enabled = enabled
+ IdentityMap.clear
+ end
+
+ response
end
end
end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
new file mode 100644
index 0000000000..eaa7deac5a
--- /dev/null
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -0,0 +1,182 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Inheritance
+ extend ActiveSupport::Concern
+
+ included do
+ # Determine whether to store the full constant name including namespace when using STI
+ config_attribute :store_full_sti_class
+ self.store_full_sti_class = true
+ end
+
+ module ClassMethods
+ # True if this isn't a concrete subclass needing a STI type condition.
+ def descends_from_active_record?
+ sup = active_record_super
+
+ if sup.abstract_class?
+ sup.descends_from_active_record?
+ elsif self == Base
+ false
+ else
+ [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column)
+ end
+ end
+
+ def finder_needs_type_condition? #:nodoc:
+ # This is like this because benchmarking justifies the strange :false stuff
+ :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
+ end
+
+ def symbolized_base_class
+ @symbolized_base_class ||= base_class.to_s.to_sym
+ end
+
+ def symbolized_sti_name
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
+ end
+
+ # Returns the base AR subclass that this class descends from. If A
+ # extends AR::Base, A.base_class will return A. If B descends from A
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
+ #
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
+ # and C.base_class would return B as the answer since A is an abstract_class.
+ def base_class
+ class_of_active_record_descendant(self)
+ end
+
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
+ attr_accessor :abstract_class
+
+ # Returns whether this class is an abstract class or not.
+ def abstract_class?
+ defined?(@abstract_class) && @abstract_class == true
+ end
+
+ def sti_name
+ store_full_sti_class ? name : name.demodulize
+ 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.
+ def instantiate(record)
+ sti_class = find_sti_class(record[inheritance_column])
+ record_id = sti_class.primary_key && record[sti_class.primary_key]
+
+ if ActiveRecord::IdentityMap.enabled? && record_id
+ if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
+ record_id = record_id.to_i
+ end
+ if instance = IdentityMap.get(sti_class, record_id)
+ instance.reinit_with('attributes' => record)
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
+ IdentityMap.add(instance)
+ end
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
+ end
+
+ instance
+ end
+
+ # For internal use.
+ #
+ # If this class includes ActiveRecord::Model then it won't have a
+ # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model).
+ def active_record_super #:nodoc:
+ superclass < Model ? superclass : Model
+ end
+
+ protected
+
+ # 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)
+ unless klass < Model
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ end
+
+ sup = klass.active_record_super
+ if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class?
+ klass
+ else
+ class_of_active_record_descendant(sup)
+ 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)
+ if type_name.match(/^::/)
+ # If the type is prefixed with a scope operator then we assume that
+ # the type_name is an absolute reference.
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ # Build a list of candidates to search for
+ candidates = []
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
+ candidates << type_name
+
+ candidates.each do |candidate|
+ begin
+ constant = ActiveSupport::Dependencies.constantize(candidate)
+ return constant if candidate == constant.to_s
+ rescue NameError => e
+ # We don't want to swallow NoMethodError < NameError errors
+ raise e unless e.instance_of?(NameError)
+ end
+ end
+
+ raise NameError, "uninitialized constant #{candidates.first}"
+ end
+ end
+
+ private
+
+ def find_sti_class(type_name)
+ if type_name.blank? || !columns_hash.include?(inheritance_column)
+ self
+ else
+ begin
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
+ "or overwrite #{name}.inheritance_column to use another column for that information."
+ end
+ end
+ end
+
+ def type_condition(table = arel_table)
+ sti_column = table[inheritance_column.to_sym]
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
+
+ sti_column.in(sti_names)
+ end
+ end
+
+ private
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the
+ # ActiveRecord::Base descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
+ # 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
+ klass = self.class
+ if klass.finder_needs_type_condition?
+ write_attribute(klass.inheritance_column, klass.sti_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
new file mode 100644
index 0000000000..2c42f4cca5
--- /dev/null
+++ b/activerecord/lib/active_record/integration.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ module Integration
+ # 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.
+ #
+ # For example, suppose that you have a User model, and that you have a
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
+ # construct a path with the user object's 'id' in it:
+ #
+ # user = User.find_by_name('Phusion')
+ # user_path(user) # => "/users/1"
+ #
+ # You can override +to_param+ in your model to make +user_path+ construct
+ # a path using the user's name instead of the user's id:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param # overridden
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by_name('Phusion')
+ # user_path(user) # => "/users/Phusion"
+ def to_param
+ # We can't use alias_method here, because method 'id' optimizes itself on the fly.
+ id && id.to_s # Be sure to stringify the id for routes
+ end
+
+ # Returns a cache key that can be used to identify this record.
+ #
+ # ==== Examples
+ #
+ # Product.new.cache_key # => "products/new"
+ # Product.find(5).cache_key # => "products/5" (updated_at not available)
+ # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
+ def cache_key
+ case
+ when new_record?
+ "#{self.class.model_name.cache_key}/new"
+ when timestamp = self[:updated_at]
+ timestamp = timestamp.utc.to_s(:number)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ else
+ "#{self.class.model_name.cache_key}/#{id}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 1a29ded787..e643c0d437 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -49,12 +49,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- cattr_accessor :lock_optimistically, :instance_writer => false
+ config_attribute :lock_optimistically, :global => true
self.lock_optimistically = true
-
- class << self
- alias_method :locking_column=, :set_locking_column
- end
end
def locking_enabled? #:nodoc:
@@ -68,21 +64,6 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def attributes_from_column_definition
- result = super
-
- # If the locking column has no default value set,
- # 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 result.key?(self.class.locking_column) && lock_optimistically
- result[self.class.locking_column] ||= 0
- end
-
- result
- end
-
def update(attribute_names = @attributes.keys) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -149,14 +130,15 @@ module ActiveRecord
end
# Set the column to use for optimistic locking. Defaults to +lock_version+.
- def set_locking_column(value = nil, &block)
- define_attr_method :locking_column, value, &block
- value
+ def locking_column=(value)
+ @original_locking_column = @locking_column if defined?(@locking_column)
+ @locking_column = value.to_s
end
# The version column used for optimistic locking. Defaults to +lock_version+.
def locking_column
- reset_locking_column
+ reset_locking_column unless defined?(@locking_column)
+ @locking_column
end
# Quote the column name used for optimistic locking.
@@ -166,7 +148,7 @@ module ActiveRecord
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
- set_locking_column DEFAULT_LOCKING_COLUMN
+ self.locking_column = DEFAULT_LOCKING_COLUMN
end
# Make sure the lock version column gets updated when counters are
@@ -175,6 +157,18 @@ module ActiveRecord
counters = counters.merge(locking_column => 1) if locking_enabled?
super
end
+
+ # If the locking column has no default value set,
+ # 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.
+ def initialize_attributes(attributes) #:nodoc:
+ if attributes.key?(locking_column) && lock_optimistically
+ attributes[locking_column] ||= 0
+ end
+
+ attributes
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 66994e4797..58af92f0b1 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -38,6 +38,18 @@ module ActiveRecord
# account2.save!
# end
#
+ # You can start a transaction and acquire the lock in one go by calling
+ # <tt>with_lock</tt> with a block. The block is called from within
+ # a transaction, the object is already locked. Example:
+ #
+ # account = Account.first
+ # account.with_lock do
+ # # This block is called within a transaction,
+ # # account is already locked.
+ # account.balance -= 100
+ # account.save!
+ # end
+ #
# Database-specific information on row locking:
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
# PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
@@ -50,6 +62,16 @@ module ActiveRecord
reload(:lock => lock) if persisted?
self
end
+
+ # Wraps the passed block in a transaction, locking the object
+ # before yielding. You pass can the SQL locking clause
+ # as argument (see <tt>lock!</tt>).
+ def with_lock(lock = true)
+ transaction do
+ lock!(lock)
+ yield
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 3a015ee8c2..a25f2c7bca 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -26,9 +26,9 @@ module ActiveRecord
return if 'SCHEMA' == payload[:name]
- name = '%s (%.1fms)' % [payload[:name], event.duration]
- sql = payload[:sql].squeeze(' ')
- binds = nil
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
+ sql = payload[:sql].squeeze(' ')
+ binds = nil
unless (payload[:binds] || []).empty?
binds = " " + payload[:binds].map { |col,v|
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index d70c7d1d34..2a9139749d 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,6 +1,8 @@
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/class/attribute_accessors"
-require "active_support/core_ext/array/wrap"
+require 'active_support/deprecation'
+require 'active_record/schema_migration'
+require 'set'
module ActiveRecord
# Exception that can be raised to stop migrations from going backwards.
@@ -302,7 +304,7 @@ module ActiveRecord
#
# class TenderloveMigration < ActiveRecord::Migration
# def change
- # create_table(:horses) do
+ # create_table(:horses) do |t|
# t.column :content, :text
# t.column :remind_at, :datetime
# end
@@ -341,16 +343,28 @@ module ActiveRecord
attr_accessor :name, :version
- def initialize
- @name = self.class.name
- @version = nil
+ def initialize(name = self.class.name, version = nil)
+ @name = name
+ @version = version
@connection = nil
+ @reverting = false
end
# instantiate the delegate object after initialize is defined
self.verbose = true
self.delegate = new
+ def revert
+ @reverting = true
+ yield
+ ensure
+ @reverting = false
+ end
+
+ def reverting?
+ @reverting
+ end
+
def up
self.class.delegate = self
return unless self.class.respond_to?(:up)
@@ -384,9 +398,11 @@ module ActiveRecord
end
@connection = conn
time = Benchmark.measure {
- recorder.inverse.each do |cmd, args|
- send(cmd, *args)
- end
+ self.revert {
+ recorder.inverse.each do |cmd, args|
+ send(cmd, *args)
+ end
+ }
}
else
time = Benchmark.measure { change }
@@ -441,8 +457,11 @@ module ActiveRecord
arg_list = arguments.map{ |a| a.inspect } * ', '
say_with_time "#{method}(#{arg_list})" do
- unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
+ unless reverting?
+ unless arguments.empty? || method == :execute
+ arguments[0] = Migrator.proper_table_name(arguments.first)
+ arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ end
end
return super unless connection.respond_to?(method)
connection.send(method, *arguments, &block)
@@ -456,26 +475,28 @@ module ActiveRecord
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
- sources.each do |name, path|
+ sources.each do |scope, path|
source_migrations = ActiveRecord::Migrator.migrations(path)
source_migrations.each do |migration|
source = File.read(migration.filename)
- source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}"
+ source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
- options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip]
+ if options[:on_skip] && duplicate.scope != scope.to_s
+ options[:on_skip].call(scope, migration)
+ end
next
end
migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
- new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb")
+ new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb")
old_path, migration.filename = migration.filename, new_path
last = migration
- FileUtils.cp(old_path, migration.filename)
+ File.open(migration.filename, "w") { |f| f.write source }
copied << migration
- options[:on_copy].call(name, migration, old_path) if options[:on_copy]
+ options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
end
end
@@ -494,9 +515,9 @@ module ActiveRecord
# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
- class MigrationProxy < Struct.new(:name, :version, :filename)
+ class MigrationProxy < Struct.new(:name, :version, :filename, :scope)
- def initialize(name, version, filename)
+ def initialize(name, version, filename, scope)
super
@migration = nil
end
@@ -505,7 +526,7 @@ module ActiveRecord
File.basename(filename)
end
- delegate :migrate, :announce, :write, :to=>:migration
+ delegate :migrate, :announce, :write, :to => :migration
private
@@ -525,16 +546,16 @@ module ActiveRecord
attr_writer :migrations_paths
alias :migrations_path= :migrations_paths=
- def migrate(migrations_paths, target_version = nil)
+ def migrate(migrations_paths, target_version = nil, &block)
case
- when target_version.nil?
- up(migrations_paths, target_version)
- when current_version == 0 && target_version == 0
- []
- when current_version > target_version
- down(migrations_paths, target_version)
- else
- up(migrations_paths, target_version)
+ when target_version.nil?
+ up(migrations_paths, target_version, &block)
+ when current_version == 0 && target_version == 0
+ []
+ when current_version > target_version
+ down(migrations_paths, target_version, &block)
+ else
+ up(migrations_paths, target_version, &block)
end
end
@@ -547,24 +568,33 @@ module ActiveRecord
end
def up(migrations_paths, target_version = nil)
- self.new(:up, migrations_paths, target_version).migrate
+ migrations = migrations(migrations_paths)
+ migrations.select! { |m| yield m } if block_given?
+
+ self.new(:up, migrations, target_version).migrate
end
- def down(migrations_paths, target_version = nil)
- self.new(:down, migrations_paths, target_version).migrate
+ def down(migrations_paths, target_version = nil, &block)
+ migrations = migrations(migrations_paths)
+ migrations.select! { |m| yield m } if block_given?
+
+ self.new(:down, migrations, target_version).migrate
end
def run(direction, migrations_paths, target_version)
- self.new(direction, migrations_paths, target_version).run
+ self.new(direction, migrations(migrations_paths), target_version).run
+ end
+
+ def open(migrations_paths)
+ self.new(:up, migrations(migrations_paths), nil)
end
def schema_migrations_table_name
- Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
+ SchemaMigration.table_name
end
def get_all_versions
- table = Arel::Table.new(schema_migrations_table_name)
- Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort
+ SchemaMigration.all.map { |x| x.version.to_i }.sort
end
def current_version
@@ -584,7 +614,7 @@ module ActiveRecord
def migrations_paths
@migrations_paths ||= ['db/migrate']
# just to not break things if someone uses: migration_path = some_string
- Array.wrap(@migrations_paths)
+ Array(@migrations_paths)
end
def migrations_path
@@ -592,25 +622,18 @@ module ActiveRecord
end
def migrations(paths)
- paths = Array.wrap(paths)
-
- files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }]
+ paths = Array(paths)
- seen = Hash.new false
+ files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
migrations = files.map do |file|
- version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
+ version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
name = name.camelize
- raise DuplicateMigrationVersionError.new(version) if seen[version]
- raise DuplicateMigrationNameError.new(name) if seen[name]
-
- seen[version] = seen[name] = true
-
- MigrationProxy.new(name, version, file)
+ MigrationProxy.new(name, version, file, scope)
end
migrations.sort_by(&:version)
@@ -619,7 +642,7 @@ module ActiveRecord
private
def move(direction, migrations_paths, steps)
- migrator = self.new(direction, migrations_paths)
+ migrator = self.new(direction, migrations(migrations_paths))
start_index = migrator.migrations.index(migrator.current_migration)
if start_index
@@ -630,19 +653,33 @@ module ActiveRecord
end
end
- def initialize(direction, migrations_paths, target_version = nil)
+ def initialize(direction, migrations, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
- Base.connection.initialize_schema_migrations_table
- @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version
+
+ @direction = direction
+ @target_version = target_version
+ @migrated_versions = nil
+
+ if Array(migrations).grep(String).empty?
+ @migrations = migrations
+ else
+ ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
+ @migrations = self.class.migrations(migrations)
+ end
+
+ validate(@migrations)
+
+ ActiveRecord::SchemaMigration.create_table
end
def current_version
- migrated.last || 0
+ migrated.sort.last || 0
end
def current_migration
migrations.detect { |m| m.version == current_version }
end
+ alias :current :current_migration
def run
target = migrations.detect { |m| m.version == @target_version }
@@ -654,96 +691,108 @@ module ActiveRecord
end
def migrate
- current = migrations.detect { |m| m.version == current_version }
- target = migrations.detect { |m| m.version == @target_version }
-
- if target.nil? && @target_version && @target_version > 0
+ if !target && @target_version && @target_version > 0
raise UnknownMigrationVersionError.new(@target_version)
end
- start = up? ? 0 : (migrations.index(current) || 0)
- finish = migrations.index(target) || migrations.size - 1
- runnable = migrations[start..finish]
+ running = runnable
- # skip the last migration if we're headed down, but not ALL the way down
- runnable.pop if down? && target
+ if block_given?
+ ActiveSupport::Deprecation.warn(<<-eomsg)
+block argument to migrate is deprecated, please filter migrations before constructing the migrator
+ eomsg
+ running.select! { |m| yield m }
+ end
- ran = []
- runnable.each do |migration|
+ running.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
- seen = migrated.include?(migration.version.to_i)
-
- # On our way up, we skip migrating the ones we've already migrated
- next if up? && seen
-
- # On our way down, we skip reverting the ones we've never migrated
- if down? && !seen
- migration.announce 'never migrated, skipping'; migration.write
- next
- end
-
begin
ddl_transaction do
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
end
- ran << migration
rescue => e
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
- ran
end
- def migrations
- @migrations ||= begin
- migrations = self.class.migrations(@migrations_paths)
- down? ? migrations.reverse : migrations
+ def runnable
+ runnable = migrations[start..finish]
+ if up?
+ runnable.reject { |m| ran?(m) }
+ else
+ # skip the last migration if we're headed down, but not ALL the way down
+ runnable.pop if target
+ runnable.find_all { |m| ran?(m) }
end
end
+ def migrations
+ down? ? @migrations.reverse : @migrations.sort_by(&:version)
+ end
+
def pending_migrations
already_migrated = migrated
- migrations.reject { |m| already_migrated.include?(m.version.to_i) }
+ migrations.reject { |m| already_migrated.include?(m.version) }
end
def migrated
- @migrated_versions ||= self.class.get_all_versions
+ @migrated_versions ||= Set.new(self.class.get_all_versions)
end
private
- def record_version_state_after_migrating(version)
- table = Arel::Table.new(self.class.schema_migrations_table_name)
-
- @migrated_versions ||= []
- if down?
- @migrated_versions.delete(version)
- stmt = table.where(table["version"].eq(version.to_s)).compile_delete
- Base.connection.delete stmt
- else
- @migrated_versions.push(version).sort!
- stmt = table.compile_insert table["version"] => version.to_s
- Base.connection.insert stmt
- end
- end
+ def ran?(migration)
+ migrated.include?(migration.version.to_i)
+ end
- def up?
- @direction == :up
- end
+ def target
+ migrations.detect { |m| m.version == @target_version }
+ end
+
+ def finish
+ migrations.index(target) || migrations.size - 1
+ end
- def down?
- @direction == :down
+ def start
+ up? ? 0 : (migrations.index(current) || 0)
+ end
+
+ def validate(migrations)
+ name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 }
+ raise DuplicateMigrationNameError.new(name) if name
+
+ version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 }
+ raise DuplicateMigrationVersionError.new(version) if version
+ end
+
+ def record_version_state_after_migrating(version)
+ if down?
+ migrated.delete(version)
+ ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all
+ else
+ migrated << version
+ ActiveRecord::SchemaMigration.create!(:version => version.to_s)
end
+ end
- # Wrap the migration in a transaction only if supported by the adapter.
- def ddl_transaction(&block)
- if Base.connection.supports_ddl_transactions?
- Base.transaction { block.call }
- else
- block.call
- end
+ def up?
+ @direction == :up
+ end
+
+ def down?
+ @direction == :down
+ end
+
+ # Wrap the migration in a transaction only if supported by the adapter.
+ def ddl_transaction
+ if Base.connection.supports_ddl_transactions?
+ Base.transaction { yield }
+ else
+ yield
end
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index ffee5a081a..4e27293cb4 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -59,7 +59,7 @@ module ActiveRecord
private
def invert_create_table(args)
- [:drop_table, args]
+ [:drop_table, [args.first]]
end
def invert_rename_table(args)
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
new file mode 100644
index 0000000000..86de5ab2fa
--- /dev/null
+++ b/activerecord/lib/active_record/model.rb
@@ -0,0 +1,109 @@
+require 'active_support/deprecation'
+
+module ActiveRecord
+ # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence.
+ # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example:
+ #
+ # class Post
+ # include ActiveRecord::Model
+ # end
+ #
+ module Model
+ module ClassMethods #:nodoc:
+ include ActiveSupport::Callbacks::ClassMethods
+ include ActiveModel::Naming
+ include QueryCache::ClassMethods
+ include ActiveSupport::Benchmarkable
+ include ActiveSupport::DescendantsTracker
+
+ include Querying
+ include Translation
+ include DynamicMatchers
+ include CounterCache
+ include Explain
+ include ConnectionHandling
+ end
+
+ def self.included(base)
+ return if base.singleton_class < ClassMethods
+
+ base.class_eval do
+ extend ClassMethods
+ Callbacks::Register.setup(self)
+ initialize_generated_modules unless self == Base
+ end
+ end
+
+ extend ActiveModel::Configuration
+ extend ActiveModel::Callbacks
+ extend ActiveModel::MassAssignmentSecurity::ClassMethods
+ extend ActiveModel::AttributeMethods::ClassMethods
+ extend Callbacks::Register
+ extend Explain
+ extend ConnectionHandling
+
+ def self.extend(*modules)
+ ClassMethods.send(:include, *modules)
+ end
+
+ include Persistence
+ include ReadonlyAttributes
+ include ModelSchema
+ include Inheritance
+ include Scoping
+ include Sanitization
+ include Integration
+ include AttributeAssignment
+ include ActiveModel::Conversion
+ include Validations
+ include Locking::Optimistic, Locking::Pessimistic
+ include AttributeMethods
+ include Callbacks, ActiveModel::Observing, Timestamp
+ include Associations
+ include IdentityMap
+ include ActiveModel::SecurePassword
+ include AutosaveAssociation, NestedAttributes
+ include Aggregations, Transactions, Reflection, Serialization, Store
+ include Core
+
+ class << self
+ def arel_engine
+ self
+ end
+
+ def abstract_class?
+ false
+ end
+
+ def inheritance_column
+ 'type'
+ end
+ end
+
+ module DeprecationProxy #:nodoc:
+ class << self
+ instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ }
+
+ def method_missing(name, *args, &block)
+ if Model.respond_to?(name)
+ Model.send(name, *args, &block)
+ else
+ ActiveSupport::Deprecation.warn(
+ "The object passed to the active_record load hook was previously ActiveRecord::Base " \
+ "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
+ "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
+ "a module rather than a class. (Model is included in Base, so anything added to Model " \
+ "will be available on Base as well.)"
+ )
+ Base.send(name, *args, &block)
+ end
+ end
+
+ alias send method_missing
+ end
+ end
+ end
+
+ # Load Base at this point, because the active_record load hook is run in that file.
+ Base
+end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
new file mode 100644
index 0000000000..261f6fa974
--- /dev/null
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -0,0 +1,307 @@
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute_accessors'
+
+module ActiveRecord
+ module ModelSchema
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Accessor for the prefix type that will be prepended to every primary key column name.
+ # The options are :table_name and :table_name_with_underscore. If the first is specified,
+ # the Product class will look for "productid" instead of "id" as the primary column. If the
+ # latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+ config_attribute :primary_key_prefix_type, :global => true
+
+ ##
+ # :singleton-method:
+ # Accessor for the name of the prefix string to prepend to every table name. So if set
+ # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
+ # etc. This is a convenient way of creating a namespace for tables in a shared database.
+ # By default, the prefix is the empty string.
+ #
+ # If you are organising your models within modules you can add a prefix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_prefix which
+ # returns your chosen prefix.
+ config_attribute :table_name_prefix
+ self.table_name_prefix = ""
+
+ ##
+ # :singleton-method:
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ config_attribute :table_name_suffix
+ self.table_name_suffix = ""
+
+ ##
+ # :singleton-method:
+ # 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.
+ config_attribute :pluralize_table_names
+ self.pluralize_table_names = true
+ end
+
+ module ClassMethods
+ # Guesses the table name (in forced lower-case) based on the name of the class in the
+ # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
+ # looks like: Reply < Message < ActiveRecord::Base, then Message is used
+ # to guess the table name even when called on Reply. The rules used to do the guess
+ # are handled by the Inflector class in Active Support, which knows almost all common
+ # English inflections. You can add new inflections in config/initializers/inflections.rb.
+ #
+ # Nested classes are given table names prefixed by the singular form of
+ # the parent's table name. Enclosing modules are not considered.
+ #
+ # ==== Examples
+ #
+ # class Invoice < ActiveRecord::Base
+ # end
+ #
+ # file class table_name
+ # invoice.rb Invoice invoices
+ #
+ # 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
+ #
+ # file class table_name
+ # invoice/lineitem.rb Invoice::Lineitem lineitems
+ #
+ # Additionally, the class-level +table_name_prefix+ is prepended and the
+ # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
+ # the table name guess for an Invoice class becomes "myapp_invoices".
+ # Invoice::Lineitem becomes "myapp_invoice_lineitems".
+ #
+ # You can also set your own table name explicitly:
+ #
+ # class Mouse < ActiveRecord::Base
+ # self.table_name = "mice"
+ # end
+ #
+ # Alternatively, you can override the table_name method to define your
+ # own computation. (Possibly using <tt>super</tt> to manipulate the default
+ # table name.) Example:
+ #
+ # class Post < ActiveRecord::Base
+ # def self.table_name
+ # "special_" + super
+ # end
+ # end
+ # Post.table_name # => "special_posts"
+ def table_name
+ reset_table_name unless defined?(@table_name)
+ @table_name
+ end
+
+ # Sets the table name explicitly. Example:
+ #
+ # class Project < ActiveRecord::Base
+ # self.table_name = "project"
+ # end
+ #
+ # You can also just define your own <tt>self.table_name</tt> method; see
+ # the documentation for ActiveRecord::Base#table_name.
+ def table_name=(value)
+ @original_table_name = @table_name if defined?(@table_name)
+ @table_name = value
+ @quoted_table_name = nil
+ @arel_table = nil
+ @relation = Relation.new(self, arel_table)
+ end
+
+ # Returns a quoted version of the table name, used to construct SQL statements.
+ def quoted_table_name
+ @quoted_table_name ||= connection.quote_table_name(table_name)
+ end
+
+ # Computes the table name, (re)sets it internally, and returns it.
+ def reset_table_name #:nodoc:
+ if abstract_class?
+ self.table_name = if active_record_super == Base || active_record_super.abstract_class?
+ nil
+ else
+ active_record_super.table_name
+ end
+ elsif active_record_super.abstract_class?
+ self.table_name = active_record_super.table_name || compute_table_name
+ else
+ self.table_name = compute_table_name
+ end
+ end
+
+ def full_table_name_prefix #:nodoc:
+ (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
+ end
+
+ # The name of the column containing the object's class when Single Table Inheritance is used
+ def inheritance_column
+ (@inheritance_column ||= nil) || active_record_super.inheritance_column
+ end
+
+ # Sets the value of inheritance_column
+ def inheritance_column=(value)
+ @original_inheritance_column = inheritance_column
+ @inheritance_column = value.to_s
+ end
+
+ def sequence_name
+ if base_class == self
+ @sequence_name ||= reset_sequence_name
+ else
+ (@sequence_name ||= nil) || base_class.sequence_name
+ end
+ end
+
+ def reset_sequence_name #:nodoc:
+ self.sequence_name = connection.default_sequence_name(table_name, primary_key)
+ end
+
+ # Sets the name of the sequence to use when generating ids to the given
+ # value, or (if the value is nil or false) to the value returned by the
+ # given block. This is required for Oracle and is useful for any
+ # database which relies on sequences for primary key generation.
+ #
+ # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # it will default to the commonly used pattern of: #{table_name}_seq
+ #
+ # If a sequence name is not explicitly set when using PostgreSQL, it
+ # will discover the sequence corresponding to your primary key for you.
+ #
+ # class Project < ActiveRecord::Base
+ # self.sequence_name = "projectseq" # default would have been "project_seq"
+ # end
+ def sequence_name=(value)
+ @original_sequence_name = @sequence_name if defined?(@sequence_name)
+ @sequence_name = value.to_s
+ end
+
+ # Indicates whether the table associated with this class exists
+ def table_exists?
+ connection.schema_cache.table_exists?(table_name)
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= connection.schema_cache.columns[table_name].map do |col|
+ col = col.dup
+ col.primary = (col.name == primary_key)
+ col
+ end
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ 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
+ @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ end
+
+ # Returns an array of column names as strings.
+ def column_names
+ @column_names ||= columns.map { |column| column.name }
+ end
+
+ # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
+ # and columns used for single table inheritance have been removed.
+ def content_columns
+ @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ end
+
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
+ # is available.
+ def column_methods_hash #:nodoc:
+ @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
+ attr_name = attr.to_s
+ methods[attr.to_sym] = attr_name
+ methods["#{attr}=".to_sym] = attr_name
+ methods["#{attr}?".to_sym] = attr_name
+ methods["#{attr}_before_type_cast".to_sym] = attr_name
+ methods
+ end
+ end
+
+ # Resets all the cached information about columns, which will cause them
+ # to be reloaded on the next request.
+ #
+ # The most common usage pattern for this method is probably in a migration,
+ # when just after creating a table you want to populate it with some default
+ # values, eg:
+ #
+ # class CreateJobLevels < ActiveRecord::Migration
+ # def up
+ # create_table :job_levels do |t|
+ # t.integer :id
+ # t.string :name
+ #
+ # t.timestamps
+ # end
+ #
+ # JobLevel.reset_column_information
+ # %w{assistant executive manager director}.each do |type|
+ # JobLevel.create(:name => type)
+ # end
+ # end
+ #
+ # def down
+ # drop_table :job_levels
+ # end
+ # end
+ def reset_column_information
+ connection.clear_cache!
+ undefine_attribute_methods
+ connection.schema_cache.clear_table_cache!(table_name) if table_exists?
+
+ @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil
+ @dynamic_methods_hash = @inheritance_column = nil
+ @arel_engine = @relation = nil
+ end
+
+ def clear_cache! # :nodoc:
+ connection.schema_cache.clear!
+ end
+
+ private
+
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = base_class.name)
+ table_name = class_name.to_s.demodulize.underscore
+ table_name = table_name.pluralize if pluralize_table_names
+ table_name
+ end
+
+ # Computes and returns a table name according to default conventions.
+ def compute_table_name
+ base = base_class
+ if self == base
+ # Nested classes are prefixed with singular parent table name.
+ if parent < ActiveRecord::Model && !parent.abstract_class?
+ contained = parent.table_name
+ contained = contained.singularize if parent.pluralize_table_names
+ contained += '_'
+ end
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+ else
+ # STI subclasses always use their superclass' table.
+ base.table_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
deleted file mode 100644
index 0313abe456..0000000000
--- a/activerecord/lib/active_record/named_scope.rb
+++ /dev/null
@@ -1,200 +0,0 @@
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/class/attribute'
-
-module ActiveRecord
- # = Active Record Named \Scopes
- module NamedScope
- extend ActiveSupport::Concern
-
- module ClassMethods
- # Returns an anonymous \scope.
- #
- # posts = Post.scoped
- # posts.size # Fires "select count(*) from posts" and returns the count
- # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
- #
- # fruits = Fruit.scoped
- # 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
- # queries, where passing intermediate values (\scopes) around as first-class
- # objects is convenient.
- #
- # You can define a \scope that applies to all finders using
- # ActiveRecord::Base.default_scope.
- def scoped(options = nil)
- if options
- scoped.apply_finder_options(options)
- else
- if current_scope
- current_scope.clone
- else
- scope = relation.clone
- scope.default_scoped = true
- scope
- end
- 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>.
- #
- # class Shirt < ActiveRecord::Base
- # scope :red, where(:color => 'red')
- # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
- # end
- #
- # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
- # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
- #
- # Note that this is simply 'syntactic sugar' for defining an actual class method:
- #
- # class Shirt < ActiveRecord::Base
- # def self.red
- # where(:color => 'red')
- # end
- # end
- #
- # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
- # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
- # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
- # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
- # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
- # all behave as if Shirt.red really was an Array.
- #
- # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
- # all shirts that are both red and dry clean only.
- # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
- # returns the number of garments for which these criteria obtain. Similarly with
- # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
- #
- # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
- # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
- #
- # class Person < ActiveRecord::Base
- # has_many :shirts
- # end
- #
- # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
- # only shirts.
- #
- # Named \scopes can also be procedural:
- #
- # class Shirt < ActiveRecord::Base
- # scope :colored, lambda { |color| where(:color => color) }
- # end
- #
- # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
- #
- # On Ruby 1.9 you can use the 'stabby lambda' syntax:
- #
- # scope :colored, ->(color) { where(:color => color) }
- #
- # Note that scopes defined with \scope will be evaluated when they are defined, rather than
- # when they are used. For example, the following would be incorrect:
- #
- # class Post < ActiveRecord::Base
- # scope :recent, where('published_at >= ?', Time.now - 1.week)
- # end
- #
- # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt>
- # class was defined, and so the resultant SQL query would always be the same. The correct
- # way to do this would be via a lambda, which will re-evaluate the scope each time
- # it is called:
- #
- # class Post < ActiveRecord::Base
- # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) }
- # end
- #
- # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
- #
- # class Shirt < ActiveRecord::Base
- # scope :red, where(:color => 'red') do
- # def dom_id
- # 'red_shirts'
- # end
- # end
- # end
- #
- # Scopes can also be used while creating/building a record.
- #
- # class Article < ActiveRecord::Base
- # scope :published, where(:published => true)
- # end
- #
- # Article.published.new.published # => true
- # Article.published.create.published # => true
- #
- # Class methods on your model are automatically available
- # on scopes. Assuming the following setup:
- #
- # class Article < ActiveRecord::Base
- # scope :published, where(:published => true)
- # scope :featured, where(:featured => true)
- #
- # def self.latest_article
- # order('published_at desc').first
- # end
- #
- # def self.titles
- # map(&:title)
- # end
- #
- # end
- #
- # We are able to call the methods like this:
- #
- # Article.published.featured.latest_article
- # Article.featured.titles
-
- def scope(name, scope_options = {})
- name = name.to_sym
- valid_scope_name?(name)
- extension = Module.new(&Proc.new) if block_given?
-
- scope_proc = lambda do |*args|
- options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
- options = scoped.apply_finder_options(options) if options.is_a?(Hash)
-
- relation = scoped.merge(options)
-
- extension ? relation.extending(extension) : relation
- end
-
- singleton_class.send(:redefine_method, name, &scope_proc)
- end
-
- protected
-
- def valid_scope_name?(name)
- if respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index d2065d701f..9e21039c4f 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -12,7 +12,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :nested_attributes_options, :instance_writer => false
+ config_attribute :nested_attributes_options
self.nested_attributes_options = {}
end
@@ -382,7 +382,7 @@ module ActiveRecord
if attributes_collection.is_a? Hash
keys = attributes_collection.keys
attributes_collection = if keys.include?('id') || keys.include?(:id)
- Array.wrap(attributes_collection)
+ [attributes_collection]
else
attributes_collection.values
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index f047a1d9fa..09ee2ba61d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,6 +1,53 @@
+require 'active_support/concern'
+
module ActiveRecord
# = Active Record Persistence
module Persistence
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Creates an object (or multiple objects) and saves it to the database, if validations pass.
+ # The resulting object is returned whether the object was saved successfully to the database or not.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
+ # attributes on the objects that are to be created.
+ #
+ # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
+ # in the +options+ parameter.
+ #
+ # ==== Examples
+ # # Create a single new object
+ # User.create(:first_name => 'Jamie')
+ #
+ # # Create a single new object using the :admin mass-assignment security role
+ # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
+ #
+ # # Create a single new object bypassing mass-assignment security
+ # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
+ #
+ # # Create an Array of new objects
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
+ #
+ # # Create a single object and pass it into a block to set other attributes.
+ # User.create(:first_name => 'Jamie') do |u|
+ # u.is_admin = false
+ # end
+ #
+ # # Creating an Array of new objects using a block, where the block is executed for each object:
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
+ # u.is_admin = false
+ # end
+ def create(attributes = nil, options = {}, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, options, &block) }
+ else
+ object = new(attributes, options, &block)
+ object.save
+ object
+ end
+ end
+ end
+
# Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the data store yet; otherwise, returns false.
def new_record?
@@ -12,8 +59,8 @@ module ActiveRecord
@destroyed
end
- # Returns if the record is persisted, i.e. it's not a new record and it was
- # not destroyed.
+ # Returns true if the record is persisted, i.e. it's not a new record and it was
+ # not destroyed, otherwise returns false.
def persisted?
!(new_record? || destroyed?)
end
@@ -162,7 +209,7 @@ module ActiveRecord
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- self.assign_attributes(attributes, options)
+ assign_attributes(attributes, options)
save
end
end
@@ -173,7 +220,7 @@ module ActiveRecord
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- self.assign_attributes(attributes, options)
+ assign_attributes(attributes, options)
save!
end
end
@@ -238,7 +285,7 @@ module ActiveRecord
clear_association_cache
IdentityMap.without do
- fresh_object = self.class.unscoped { self.class.find(self.id, options) }
+ fresh_object = self.class.unscoped { self.class.find(id, options) }
@attributes.update(fresh_object.instance_variable_get('@attributes'))
end
@@ -321,13 +368,5 @@ module ActiveRecord
@new_record = false
id
end
-
- # Initializes the attributes array with keys matching the columns from the linked table and
- # the values matching the corresponding default value of that column, so
- # 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
- 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 466d148901..9701898415 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -27,47 +27,22 @@ module ActiveRecord
@app = app
end
- class BodyProxy # :nodoc:
- 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)
- @target.each(&block)
- end
+ def call(env)
+ enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection_id = ActiveRecord::Base.connection_id
+ ActiveRecord::Base.connection.enable_query_cache!
- def close
- @target.close if @target.respond_to?(:close)
- ensure
- ActiveRecord::Base.connection_id = @connection_id
+ response = @app.call(env)
+ response[2] = Rack::BodyProxy.new(response[2]) do
+ ActiveRecord::Base.connection_id = connection_id
ActiveRecord::Base.connection.clear_query_cache
- unless @original_cache_value
- ActiveRecord::Base.connection.disable_query_cache!
- end
+ ActiveRecord::Base.connection.disable_query_cache! unless enabled
end
- end
- def call(env)
- old = ActiveRecord::Base.connection.query_cache_enabled
- ActiveRecord::Base.connection.enable_query_cache!
-
- status, headers, body = @app.call(env)
- [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)]
+ response
rescue Exception => e
ActiveRecord::Base.connection.clear_query_cache
- unless old
- ActiveRecord::Base.connection.disable_query_cache!
- end
+ ActiveRecord::Base.connection.disable_query_cache! unless enabled
raise e
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
new file mode 100644
index 0000000000..94e34e1bd4
--- /dev/null
+++ b/activerecord/lib/active_record/querying.rb
@@ -0,0 +1,58 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module Querying
+ 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, :uniq, :references, :to => :scoped
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
+
+ # Executes a custom SQL query against your database and returns all the results. The results will
+ # be returned as an array with columns requested encapsulated as attributes of the model you call
+ # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
+ # a Product object with the attributes you specified in the SQL query.
+ #
+ # If you call a complicated SQL query which spans multiple tables the columns specified by the
+ # SELECT will be attributes of the model, whether or not they are columns of the corresponding
+ # table.
+ #
+ # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
+ # no database agnostic conversions performed. This should be a last resort because using, for example,
+ # MySQL specific terms will lock you to using that particular database engine or require you to
+ # change your call if you switch engines.
+ #
+ # ==== Examples
+ # # A simple SQL query spanning multiple tables
+ # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ #
+ # # You can use the same string replacement techniques as you can with ActiveRecord#find
+ # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
+ # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ def find_by_sql(sql, binds = [])
+ logging_query_plan do
+ connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
+ end
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # The use of this method should be restricted to complicated SQL queries that can't be executed
+ # using the ActiveRecord::Calculations class methods. Look into those before using this.
+ #
+ # ==== Parameters
+ #
+ # * +sql+ - An SQL statement which should return a count query from the database, see the example below.
+ #
+ # ==== Examples
+ #
+ # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 47133e77e8..058dd58efb 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -22,6 +22,13 @@ module ActiveRecord
config.app_middleware.insert_after "::ActionDispatch::Callbacks",
"ActiveRecord::ConnectionAdapters::ConnectionManagement"
+ config.action_dispatch.rescue_responses.merge!(
+ 'ActiveRecord::RecordNotFound' => :not_found,
+ 'ActiveRecord::StaleObjectError' => :conflict,
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
+ )
+
rake_tasks do
load "active_record/railties/databases.rake"
end
@@ -31,7 +38,8 @@ module ActiveRecord
# first time. Also, make it output to STDERR.
console do |app|
require "active_record/railties/console_sandbox" if app.sandbox?
- ActiveRecord::Base.logger = Logger.new(STDERR)
+ console = ActiveSupport::Logger.new(STDERR)
+ Rails.logger.extend ActiveSupport::Logger.broadcast console
end
initializer "active_record.initialize_timezone" do
@@ -78,18 +86,30 @@ module ActiveRecord
end
end
- initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app|
- ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.to_cleanup do
- ActiveRecord::Base.clear_reloadable_connections!
- ActiveRecord::Base.clear_cache!
+ initializer "active_record.set_reloader_hooks" do |app|
+ hook = lambda do
+ ActiveRecord::Base.clear_reloadable_connections!
+ ActiveRecord::Base.clear_cache!
+ end
+
+ if app.config.reload_classes_only_on_change
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.to_prepare(&hook)
+ end
+ else
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.to_cleanup(&hook)
end
end
end
+ initializer "active_record.add_watchable_files" do |app|
+ config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
+ end
+
config.after_initialize do
ActiveSupport.on_load(:active_record) do
- instantiate_observers
+ ActiveRecord::Base.instantiate_observers
ActionDispatch::Reloader.to_prepare do
ActiveRecord::Base.instantiate_observers
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index abd71793fd..822b51e838 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -38,6 +38,7 @@ db_namespace = namespace :db do
desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
task :create => :load_config do
configs_for_environment.each { |config| create_database(config) }
+ ActiveRecord::Base.establish_connection(configs_for_environment.first)
end
def mysql_creation_options(config)
@@ -111,8 +112,7 @@ db_namespace = namespace :db do
end
end
else
- # Bug with 1.9.2 Calling return within begin still executes else
- $stderr.puts "#{config['database']} already exists" unless config['adapter'] =~ /sqlite/
+ $stderr.puts "#{config['database']} already exists"
end
end
@@ -149,7 +149,9 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
+ ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
+ end
db_namespace['_dump'].invoke
end
@@ -369,36 +371,37 @@ db_namespace = namespace :db do
end
namespace :structure do
- desc 'Dump the database structure to an SQL file'
+ desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => :environment do
abcs = ActiveRecord::Base.configurations
+ filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case abcs[Rails.env]['adapter']
when /mysql/, 'oci', 'oracle'
ActiveRecord::Base.establish_connection(abcs[Rails.env])
- File.open("#{Rails.root}/db/structure.sql", "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
when /postgresql/
set_psql_env(abcs[Rails.env])
search_path = abcs[Rails.env]['schema_search_path']
unless search_path.blank?
search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ")
end
- `pg_dump -i -s -x -O -f db/structure.sql #{search_path} #{abcs[Rails.env]['database']}`
+ `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}`
raise 'Error dumping database' if $?.exitstatus == 1
when /sqlite/
dbfile = abcs[Rails.env]['database']
- `sqlite3 #{dbfile} .schema > db/structure.sql`
+ `sqlite3 #{dbfile} .schema > #{filename}`
when 'sqlserver'
- `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\structure.sql -A -U`
+ `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U`
when "firebird"
set_firebird_env(abcs[Rails.env])
db_string = firebird_db_string(abcs[Rails.env])
- sh "isql -a #{db_string} > #{Rails.root}/db/structure.sql"
+ sh "isql -a #{db_string} > #{filename}"
else
raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'"
end
if ActiveRecord::Base.connection.supports_migrations?
- File.open("#{Rails.root}/db/structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
end
end
@@ -407,30 +410,31 @@ db_namespace = namespace :db do
env = ENV['RAILS_ENV'] || 'test'
abcs = ActiveRecord::Base.configurations
+ filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case abcs[env]['adapter']
when /mysql/
ActiveRecord::Base.establish_connection(abcs[env])
ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
- IO.read("#{Rails.root}/db/structure.sql").split("\n\n").each do |table|
+ IO.read(filename).split("\n\n").each do |table|
ActiveRecord::Base.connection.execute(table)
end
when /postgresql/
set_psql_env(abcs[env])
- `psql -f "#{Rails.root}/db/structure.sql" #{abcs[env]['database']} #{abcs[env]['template']}`
+ `psql -f "#{filename}" #{abcs[env]['database']} #{abcs[env]['template']}`
when /sqlite/
dbfile = abcs[env]['database']
- `sqlite3 #{dbfile} < "#{Rails.root}/db/structure.sql"`
+ `sqlite3 #{dbfile} < "#{filename}"`
when 'sqlserver'
- `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i db\\structure.sql`
+ `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}`
when 'oci', 'oracle'
ActiveRecord::Base.establish_connection(abcs[env])
- IO.read("#{Rails.root}/db/structure.sql").split(";\n\n").each do |ddl|
+ IO.read(filename).split(";\n\n").each do |ddl|
ActiveRecord::Base.connection.execute(ddl)
end
when 'firebird'
set_firebird_env(abcs[env])
db_string = firebird_db_string(abcs[env])
- sh "isql -i #{Rails.root}/db/structure.sql #{db_string}"
+ sh "isql -i #{filename} #{db_string}"
else
raise "Task not supported by '#{abcs[env]['adapter']}'"
end
@@ -442,26 +446,39 @@ db_namespace = namespace :db do
end
namespace :test do
- # desc "Recreate the test database from the current schema.rb"
+
+ # desc "Recreate the test database from the current schema"
task :load => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
+ case ActiveRecord::Base.schema_format
+ when :ruby
+ db_namespace["test:load_schema"].invoke
+ when :sql
+ db_namespace["test:load_structure"].invoke
+ end
+ end
+ # desc "Recreate the test database from an existent structure.sql file"
+ task :load_structure => 'db:test:purge' do
begin
old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test'
- db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql
+ db_namespace["structure:load"].invoke
ensure
ENV['RAILS_ENV'] = old_env
end
+ end
+ # desc "Recreate the test database from an existent schema.rb file"
+ task :load_schema => 'db:test:purge' do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
end
- # desc "Recreate the test database from the current environment's database schema"
- task :clone => %w(db:schema:dump db:test:load)
+ # desc "Recreate the test database from a fresh schema.rb file"
+ task :clone => %w(db:schema:dump db:test:load_schema)
- # desc "Recreate the test databases from the structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load" ]
+ # desc "Recreate the test database from a fresh structure.sql file"
+ task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
# desc "Empty the test database"
task :purge => :environment do
@@ -522,7 +539,7 @@ end
namespace :railties do
namespace :install do
- # desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2"
+ # desc "Copies missing migrations from Railties (e.g. 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 = ActiveSupport::OrderedHash.new
diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
index 6b9af2a0cb..0b75983484 100644
--- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb
+++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
@@ -1,5 +1,5 @@
#FIXME Remove if ArJdbcMysql will give.
-module ArJdbcMySQL
+module ArJdbcMySQL #:nodoc:
class Error < StandardError
attr_accessor :error_number, :sql_state
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
new file mode 100644
index 0000000000..836b15e2ce
--- /dev/null
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -0,0 +1,26 @@
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ module ReadonlyAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ config_attribute :_attr_readonly
+ self._attr_readonly = []
+ end
+
+ module ClassMethods
+ # Attributes listed as readonly will be used to create a new record but update operations will
+ # ignore these fields.
+ def attr_readonly(*attributes)
+ self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
+ end
+
+ # Returns an array of all the attributes that have been specified as readonly.
+ def readonly_attributes
+ self._attr_readonly
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 52968070cb..f02f0544c5 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -7,7 +7,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :reflections
+ extend ActiveModel::Configuration
+ config_attribute :reflections
self.reflections = {}
end
@@ -174,7 +175,7 @@ module ActiveRecord
def initialize(macro, name, options, active_record)
super
- @collection = macro.in?([:has_many, :has_and_belongs_to_many])
+ @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
end
# Returns a new, unsaved instance of the associated class. +options+ will
@@ -262,6 +263,10 @@ module ActiveRecord
[self]
end
+ def nested?
+ false
+ end
+
# An array of arrays of conditions. Each item in the outside array corresponds to a reflection
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
# a hash, array, arel predicate, etc...)
@@ -457,7 +462,7 @@ module ActiveRecord
source_reflection.source_macro
end
- # A through association is nested iff there would be more than one join table
+ # A through association is nested if there would be more than one join table
def nested?
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 0c32ad5139..ac70aeba67 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,20 +1,16 @@
# -*- coding: utf-8 -*-
require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/module/delegation'
+require 'active_support/deprecation'
module ActiveRecord
# = Active Record Relation
class Relation
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, :from, :reorder, :reverse_order, :uniq]
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq]
- 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, :connection, :column_hash,:to => :klass
+ include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
@@ -137,30 +133,35 @@ module ActiveRecord
first || new(attributes, options, &block)
end
- def respond_to?(method, include_private = false)
- arel.respond_to?(method, include_private) ||
- Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
- super
- end
-
+ # Runs EXPLAIN on the query or queries triggered by this relation and
+ # returns the result as a string. The string is formatted imitating the
+ # ones printed by the database shell.
+ #
+ # Note that this method actually runs the queries, since the results of some
+ # are needed by the next ones when eager loading is going on.
+ #
+ # Please see further details in the
+ # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- queries = []
- callback = lambda do |*args|
- payload = args.last
- queries << payload[:sql] unless payload[:exception] || %w(SCHEMA EXPLAIN).include?(payload[:name])
- end
+ _, queries = collecting_queries_for_explain { exec_queries }
+ exec_explain(queries)
+ end
- ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
- to_a
+ def to_a
+ # We monitor here the entire execution rather than individual SELECTs
+ # because from the point of view of the user fetching the records of a
+ # relation is a single unit of work. You want to know if this call takes
+ # too long, not if the individual queries take too long.
+ #
+ # It could be the case that none of the queries involved surpass the
+ # threshold, and at the same time the sum of them all does. The user
+ # should get a query plan logged in that case.
+ logging_query_plan do
+ exec_queries
end
-
- queries.map do |sql|
- "EXPLAIN for: #{sql}\n#{@klass.connection.explain(sql)}"
- end.join("\n")
end
- def to_a
+ def exec_queries
return @records if loaded?
default_scoped = with_default_scope
@@ -191,6 +192,7 @@ module ActiveRecord
@loaded = true
@records
end
+ private :exec_queries
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
@@ -236,7 +238,7 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @klass.send(:with_scope, self, :overwrite) { yield }
+ @klass.with_scope(self, :overwrite) { yield }
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
@@ -354,7 +356,7 @@ module ActiveRecord
end
end
- # Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
+ # 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
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
#
@@ -494,6 +496,10 @@ module ActiveRecord
to_a.inspect
end
+ def pretty_print(q)
+ q.pp(self.to_a)
+ end
+
def with_default_scope #:nodoc:
if default_scoped? && default_scope = klass.send(:build_default_scope)
default_scope = default_scope.merge(self)
@@ -504,20 +510,6 @@ module ActiveRecord
end
end
- protected
-
- def method_missing(method, *args, &block)
- if Array.method_defined?(method)
- to_a.send(method, *args, &block)
- elsif @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
- else
- super
- end
- end
-
private
def references_eager_loaded_tables?
@@ -533,8 +525,30 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
-
- (tables_in_string(to_sql) - joined_tables).any?
+ string_tables = tables_in_string(to_sql)
+
+ if (references_values - joined_tables).any?
+ true
+ elsif (string_tables - joined_tables).any?
+ ActiveSupport::Deprecation.warn(
+ "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
+ "that are referenced in a string SQL snippet. For example: \n" \
+ "\n" \
+ " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
+ "\n" \
+ "Currently, Active Record recognises the table in the string, and knows to JOIN the " \
+ "comments table to the query, rather than loading comments in a separate query. " \
+ "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
+ "Since we don't want to write an SQL parser, we are removing this functionality. " \
+ "From now on, you must explicitly tell Active Record when you are referencing a table " \
+ "from a string:\n" \
+ "\n" \
+ " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n"
+ )
+ true
+ else
+ false
+ end
end
def tables_in_string(string)
@@ -543,6 +557,5 @@ module ActiveRecord
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
end
-
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index af86771d2d..bf9b4bf1c9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -166,6 +166,22 @@ module ActiveRecord
0
end
+ # This method is designed to perform select by a single column as direct SQL query
+ # Returns <tt>Array</tt> with values of the specified column name
+ # The values has same data type as column.
+ #
+ # Examples:
+ #
+ # Person.pluck(:id) # SELECT people.id FROM people
+ # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
+ # Person.where(:confirmed => true).limit(5).pluck(:id)
+ #
+ def pluck(column_name)
+ klass.connection.select_all(select(column_name).arel).map! do |attributes|
+ klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes))
+ end
+ end
+
private
def perform_calculation(operation, column_name, options = {})
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
new file mode 100644
index 0000000000..f5fdf437bf
--- /dev/null
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -0,0 +1,49 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module Delegation
+ # Set up common delegations for performance (avoids method_missing)
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
+ :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
+
+ def self.delegate_to_scoped_klass(method)
+ if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.send(#{method.inspect}, *args, &block) }
+ end
+ RUBY
+ end
+ end
+
+ def respond_to?(method, include_private = false)
+ super || Array.method_defined?(method) ||
+ @klass.respond_to?(method, include_private) ||
+ arel.respond_to?(method, include_private)
+ end
+
+ protected
+
+ def method_missing(method, *args, &block)
+ if Array.method_defined?(method)
+ ::ActiveRecord::Delegation.delegate method, :to => :to_a
+ to_a.send(method, *args, &block)
+ elsif @klass.respond_to?(method)
+ ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
+ scoping { @klass.send(method, *args, &block) }
+ elsif arel.respond_to?(method)
+ ::ActiveRecord::Delegation.delegate method, :to => :arel
+ arel.send(method, *args, &block)
+ else
+ super
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 3c8e0f2052..f1ac421a50 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -134,7 +134,7 @@ module ActiveRecord
def last(*args)
if args.any?
if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
- if order_values.empty? && reorder_value.nil?
+ if order_values.empty?
order("#{primary_key} DESC").limit(*args).reverse
else
to_a.last(*args)
@@ -187,7 +187,7 @@ module ActiveRecord
def exists?(id = false)
return false if id.nil?
- id = id.id if ActiveRecord::Base === id
+ id = id.id if ActiveRecord::Model === id
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index a789f48725..1d04e763f6 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -15,44 +15,61 @@ module ActiveRecord
table = Arel::Table.new(table_name, engine)
end
- attribute = table[column.to_sym]
-
- case value
- when ActiveRecord::Relation
- 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| x.is_a?(ActiveRecord::Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
-
- array_predicates = ranges.map {|range| attribute.in(range)}
-
- if values.include?(nil)
- values = values.compact
- if values.empty?
- array_predicates << attribute.eq(nil)
- else
- array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
- end
+ build(table[column.to_sym], value)
+ end
+ end
+ predicates.flatten
+ end
+
+ def self.references(attributes)
+ references = attributes.map do |key, value|
+ if value.is_a?(Hash)
+ key
+ else
+ key = key.to_s
+ key.split('.').first.to_sym if key.include?('.')
+ end
+ end
+ references.compact
+ end
+
+ private
+ def self.build(attribute, value)
+ case value
+ when ActiveRecord::Relation
+ 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| x.is_a?(ActiveRecord::Model) ? x.id : x}
+ ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
+
+ array_predicates = ranges.map {|range| attribute.in(range)}
+
+ if values.include?(nil)
+ values = values.compact
+ case values.length
+ when 0
+ array_predicates << attribute.eq(nil)
+ when 1
+ array_predicates << attribute.eq(values.first).or(attribute.eq(nil))
else
- array_predicates << attribute.in(values)
+ array_predicates << attribute.in(values).or(attribute.eq(nil))
end
-
- array_predicates.inject {|composite, predicate| composite.or(predicate)}
- when Range, Arel::Relation
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
else
- attribute.eq(value)
+ array_predicates << attribute.in(values)
end
+
+ array_predicates.inject {|composite, predicate| composite.or(predicate)}
+ when Range, Arel::Relation
+ attribute.in(value)
+ when ActiveRecord::Model
+ attribute.eq(value.id)
+ when Class
+ # FIXME: I think we need to deprecate this behavior
+ attribute.eq(value.name)
+ else
+ attribute.eq(value)
end
end
-
- predicates.flatten
- end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index c281bead0d..a8ae7208fc 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -9,8 +9,8 @@ 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, :reverse_order_value,
- :uniq_value
+ :from_value, :reordering_value, :reverse_order_value,
+ :uniq_value, :references_values
def includes(*args)
args.reject! {|a| a.blank? }
@@ -38,6 +38,24 @@ module ActiveRecord
relation
end
+ # Used to indicate that an association is referenced by an SQL string, and should
+ # therefore be JOINed in any query rather than loaded separately.
+ #
+ # For example:
+ #
+ # User.includes(:posts).where("posts.name = 'foo'")
+ # # => Doesn't JOIN the posts table, resulting in an error.
+ #
+ # User.includes(:posts).where("posts.name = 'foo'").references(:posts)
+ # # => Query now knows the string references posts, so adds a JOIN
+ def references(*args)
+ return self if args.blank?
+
+ relation = clone
+ relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq
+ relation
+ end
+
# Works in two unique ways.
#
# First: takes a block so it can be used just like Array#select.
@@ -57,16 +75,16 @@ module ActiveRecord
# 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:
+ # The argument to the method can also be an array of fields.
#
- # >> Model.select(:field, :other_field, :and_one_more)
+ # >> 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:
+ # Accessing attributes of an object that do not have fields retrieved by a select
+ # will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
# >> Model.select(:field).first.other_field
- # => nil
+ # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(value = Proc.new)
if block_given?
to_a.select {|*block_args| value.call(*block_args) }
@@ -88,16 +106,33 @@ module ActiveRecord
def order(*args)
return self if args.blank?
+ args = args.flatten
+ references = args.reject { |arg| Arel::Node === arg }
+ .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }
+ .compact
+
relation = clone
- relation.order_values += args.flatten
+ relation = relation.references(references) if references.any?
+ relation.order_values += args
relation
end
+ # Replaces any existing order defined on the relation with the specified order.
+ #
+ # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
+ #
+ # Subsequent calls to order on the same relation will be appended. For example:
+ #
+ # User.order('email DESC').reorder('id ASC').order('name ASC')
+ #
+ # generates a query with 'ORDER BY id ASC, name ASC'.
+ #
def reorder(*args)
return self if args.blank?
relation = clone
- relation.reorder_value = args.flatten
+ relation.reordering_value = true
+ relation.order_values = args.flatten
relation
end
@@ -122,6 +157,7 @@ module ActiveRecord
return self if opts.blank?
relation = clone
+ relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
relation.where_values += build_where(opts, rest)
relation
end
@@ -130,6 +166,7 @@ module ActiveRecord
return self if opts.blank?
relation = clone
+ relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
relation.having_values += build_where(opts, rest)
relation
end
@@ -263,7 +300,7 @@ module ActiveRecord
arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
- order = @reorder_value ? @reorder_value : @order_values
+ order = @order_values
order = reverse_sql_order(order) if @reverse_order_value
arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index ba882beca9..7131aa29b6 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -22,7 +22,7 @@ module ActiveRecord
end
end
- (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method|
+ (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method|
value = r.send(:"#{method}_values")
merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present?
end
@@ -48,7 +48,7 @@ module ActiveRecord
merged_relation.where_values = merged_wheres
- (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method|
+ (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method|
value = r.send(:"#{method}_value")
merged_relation.send(:"#{method}_value=", value) unless value.nil?
end
@@ -57,6 +57,15 @@ module ActiveRecord
merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty?
+ if (r.reordering_value)
+ # override any order specified in the original relation
+ merged_relation.reordering_value = true
+ merged_relation.order_values = r.order_values
+ else
+ # merge in order_values from r
+ merged_relation.order_values += r.order_values
+ end
+
# Apply scope extension modules
merged_relation.send :apply_modules, r.extensions
@@ -113,7 +122,7 @@ module ActiveRecord
result
end
- VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend,
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references,
:order, :select, :readonly, :group, :having, :from, :lock ]
def apply_finder_options(options)
@@ -124,7 +133,7 @@ module ActiveRecord
finders = options.dup
finders.delete_if { |key, value| value.nil? && key != :limit }
- ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder|
+ ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder|
relation = relation.send(finder, finders[finder])
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
new file mode 100644
index 0000000000..2d7d83d160
--- /dev/null
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -0,0 +1,194 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Sanitization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def quote_value(value, column = nil) #:nodoc:
+ connection.quote(value,column)
+ end
+
+ # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
+ def sanitize(object) #:nodoc:
+ connection.quote(object)
+ end
+
+ protected
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a WHERE clause.
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
+ # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ return nil if condition.blank?
+
+ case condition
+ when Array; sanitize_sql_array(condition)
+ when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
+ else condition
+ end
+ end
+ alias_method :sanitize_sql, :sanitize_sql_for_conditions
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a SET clause.
+ # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
+ def sanitize_sql_for_assignment(assignments)
+ case assignments
+ when Array; sanitize_sql_array(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments)
+ else assignments
+ end
+ end
+
+ # Accepts a hash of SQL conditions and replaces those attributes
+ # that correspond to a +composed_of+ relationship with their expanded
+ # aggregate attribute values.
+ # Given:
+ # class Person < ActiveRecord::Base
+ # composed_of :address, :class_name => "Address",
+ # :mapping => [%w(address_street street), %w(address_city city)]
+ # end
+ # Then:
+ # { :address => Address.new("813 abc st.", "chicago") }
+ # # => { :address_street => "813 abc st.", :address_city => "chicago" }
+ def expand_hash_conditions_for_aggregates(attrs)
+ expanded_attrs = {}
+ attrs.each do |attr, value|
+ unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
+ mapping = aggregate_mapping(aggregation)
+ mapping.each do |field_attr, aggregate_attr|
+ if mapping.size == 1 && !value.respond_to?(aggregate_attr)
+ expanded_attrs[field_attr] = value
+ else
+ expanded_attrs[field_attr] = value.send(aggregate_attr)
+ end
+ end
+ else
+ expanded_attrs[attr] = value
+ end
+ end
+ expanded_attrs
+ end
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
+ # { :name => "foo'bar", :group_id => 4 }
+ # # => "name='foo''bar' and group_id= 4"
+ # { :status => nil, :group_id => [1,2,3] }
+ # # => "status IS NULL and group_id IN (1,2,3)"
+ # { :age => 13..18 }
+ # # => "age BETWEEN 13 AND 18"
+ # { 'other_records.id' => 7 }
+ # # => "`other_records`.`id` = 7"
+ # { :other_records => { :id => 7 } }
+ # # => "`other_records`.`id` = 7"
+ # And for value objects on a composed_of relationship:
+ # { :address => Address.new("123 abc st.", "chicago") }
+ # # => "address_street='123 abc st.' and address_city='chicago'"
+ def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = expand_hash_conditions_for_aggregates(attrs)
+
+ table = Arel::Table.new(table_name).alias(default_table_name)
+ PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
+ connection.visitor.accept b
+ }.join(' AND ')
+ end
+ alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
+ # { :status => nil, :group_id => 1 }
+ # # => "status = NULL , group_id = 1"
+ def sanitize_sql_hash_for_assignment(attrs)
+ attrs.map do |attr, value|
+ "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ end.join(', ')
+ end
+
+ # Accepts an array of conditions. The array has each value
+ # sanitized and interpolated into the SQL statement.
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ def sanitize_sql_array(ary)
+ statement, *values = ary
+ if values.first.is_a?(Hash) && statement =~ /:\w+/
+ replace_named_bind_variables(statement, values.first)
+ elsif statement.include?('?')
+ replace_bind_variables(statement, values)
+ elsif statement.blank?
+ statement
+ else
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
+ end
+ end
+
+ alias_method :sanitize_conditions, :sanitize_sql
+
+ def replace_bind_variables(statement, values) #:nodoc:
+ raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
+ bound = values.dup
+ c = connection
+ statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ end
+
+ def replace_named_bind_variables(statement, bind_vars) #:nodoc:
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do
+ if $1 == ':' # skip postgresql casts
+ $& # return the whole match
+ elsif bind_vars.include?(match = $2.to_sym)
+ quote_bound_value(bind_vars[match])
+ else
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+ end
+ end
+ end
+
+ def expand_range_bind_variables(bind_vars) #:nodoc:
+ expanded = []
+
+ bind_vars.each do |var|
+ next if var.is_a?(Hash)
+
+ if var.is_a?(Range)
+ expanded << var.first
+ expanded << var.last
+ else
+ expanded << var
+ end
+ end
+
+ expanded
+ end
+
+ def quote_bound_value(value, c = connection) #:nodoc:
+ if value.respond_to?(:map) && !value.acts_like?(:string)
+ if value.respond_to?(:empty?) && value.empty?
+ c.quote(nil)
+ else
+ value.map { |v| c.quote(v) }.join(',')
+ end
+ else
+ c.quote(value)
+ end
+ end
+
+ def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ unless expected == provided
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
+ end
+ end
+ end
+
+ # TODO: Deprecate this
+ def quoted_id #:nodoc:
+ quote_value(id, column_for_attribute(self.class.primary_key))
+ end
+
+ private
+
+ # Quote strings appropriately for SQL statements.
+ def quote_value(value, column = nil)
+ self.class.connection.quote(value, column)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index cdde5cf3b9..2a565b51c6 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -112,7 +112,7 @@ HEADER
# 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) }
+ spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
'decimal'
else
column.type.to_s
@@ -127,10 +127,14 @@ HEADER
end.compact
# find all migration keys used in this table
- keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map{ |k| k.keys }.flatten
+ keys = [:name, :limit, :precision, :scale, :default, :null]
# figure out the lengths for each column based on above keys
- lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
+ lengths = keys.map { |key|
+ column_specs.map { |spec|
+ spec[key] ? spec[key].length + 2 : 0
+ }.max
+ }
# the string we're going to sprintf our values against, with standardized column widths
format_string = lengths.map{ |len| "%-#{len}s" }
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
new file mode 100644
index 0000000000..257963c2ce
--- /dev/null
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -0,0 +1,32 @@
+require 'active_record/scoping/default'
+require 'active_record/scoping/named'
+require 'active_record/base'
+
+module ActiveRecord
+ class SchemaMigration < ActiveRecord::Base
+ def self.table_name
+ Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
+ end
+
+ def self.create_table
+ unless connection.table_exists?(table_name)
+ connection.create_table(table_name, :id => false) do |t|
+ t.column :version, :string, :null => false
+ end
+ connection.add_index table_name, :version, :unique => true,
+ :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
+ end
+ end
+
+ def self.drop_table
+ if connection.table_exists?(table_name)
+ connection.remove_index table_name, :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
+ connection.drop_table(table_name)
+ end
+ end
+
+ def version
+ super.to_i
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
new file mode 100644
index 0000000000..a8f5e96190
--- /dev/null
+++ b/activerecord/lib/active_record/scoping.rb
@@ -0,0 +1,152 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Scoping
+ extend ActiveSupport::Concern
+
+ included do
+ include Default
+ include Named
+ end
+
+ module ClassMethods
+ # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
+ # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
+ # <tt>:create</tt> parameters are an attributes hash.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.create_with_scope
+ # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
+ # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
+ # a = create(1)
+ # a.blog_id # => 1
+ # end
+ # end
+ # end
+ #
+ # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
+ # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
+ #
+ # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
+ # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
+ # array of strings format for your joins.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.find_with_scope
+ # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
+ # with_scope(:find => limit(10)) do
+ # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
+ # end
+ # with_scope(:find => where(:author_id => 3)) do
+ # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
+ # end
+ # end
+ # end
+ # end
+ #
+ # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.find_with_exclusive_scope
+ # with_scope(:find => where(:blog_id => 1).limit(1)) do
+ # with_exclusive_scope(:find => limit(10)) do
+ # all # => SELECT * from articles LIMIT 10
+ # end
+ # end
+ # end
+ # end
+ #
+ # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
+ def with_scope(scope = {}, action = :merge, &block)
+ # If another Active Record class has been passed in, get its current scope
+ scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
+
+ previous_scope = self.current_scope
+
+ if scope.is_a?(Hash)
+ # Dup first and second level of hash (method and params).
+ scope = scope.dup
+ scope.each do |method, params|
+ scope[method] = params.dup unless params == true
+ end
+
+ scope.assert_valid_keys([ :find, :create ])
+ relation = construct_finder_arel(scope[:find] || {})
+ relation.default_scoped = true unless action == :overwrite
+
+ if previous_scope && previous_scope.create_with_value && scope[:create]
+ scope_for_create = if action == :merge
+ previous_scope.create_with_value.merge(scope[:create])
+ else
+ scope[:create]
+ end
+
+ relation = relation.create_with(scope_for_create)
+ else
+ scope_for_create = scope[:create]
+ scope_for_create ||= previous_scope.create_with_value if previous_scope
+ relation = relation.create_with(scope_for_create) if scope_for_create
+ end
+
+ scope = relation
+ end
+
+ scope = previous_scope.merge(scope) if previous_scope && action == :merge
+
+ self.current_scope = scope
+ begin
+ yield
+ ensure
+ self.current_scope = previous_scope
+ end
+ end
+
+ protected
+
+ # Works like with_scope, but discards any nested properties.
+ def with_exclusive_scope(method_scoping = {}, &block)
+ if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
+ raise ArgumentError, <<-MSG
+ New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
+
+ User.unscoped.where(:active => true)
+
+ Or call unscoped with a block:
+
+ User.unscoped do
+ User.where(:active => true).all
+ end
+
+ MSG
+ end
+ with_scope(method_scoping, :overwrite, &block)
+ end
+
+ def current_scope #:nodoc:
+ Thread.current["#{self}_current_scope"]
+ end
+
+ def current_scope=(scope) #:nodoc:
+ Thread.current["#{self}_current_scope"] = scope
+ end
+
+ private
+
+ def construct_finder_arel(options = {}, scope = nil)
+ relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
+ relation = scope.merge(relation) if scope
+ relation
+ end
+
+ end
+
+ def populate_with_current_scope_attributes
+ return unless self.class.scope_attributes?
+
+ self.class.scope_attributes.each do |att,value|
+ send("#{att}=", value) if respond_to?("#{att}=")
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
new file mode 100644
index 0000000000..5f05d146f2
--- /dev/null
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -0,0 +1,142 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Scoping
+ module Default
+ extend ActiveSupport::Concern
+
+ included do
+ # Stores the default scope for the class
+ config_attribute :default_scopes
+ self.default_scopes = []
+ end
+
+ module ClassMethods
+ # Returns a scope for the model without the default_scope.
+ #
+ # class Post < ActiveRecord::Base
+ # def self.default_scope
+ # where :published => true
+ # end
+ # end
+ #
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ #
+ # This method also accepts a block. All queries inside the block will
+ # not use the default_scope:
+ #
+ # Post.unscoped {
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
+ # }
+ #
+ # It is recommended to use the block form of unscoped because chaining
+ # unscoped with <tt>scope</tt> does not work. Assuming that
+ # <tt>published</tt> is a <tt>scope</tt>, the following two statements
+ # are equal: the default_scope is applied on both.
+ #
+ # Post.unscoped.published
+ # Post.published
+ def unscoped #:nodoc:
+ block_given? ? relation.scoping { yield } : relation
+ end
+
+ def before_remove_const #:nodoc:
+ self.current_scope = nil
+ end
+
+ protected
+
+ # Use this macro in your model to set a default scope for all operations on
+ # the model.
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope where(:published => true)
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true
+ #
+ # The <tt>default_scope</tt> is also applied while creating/building a record. It is not
+ # applied while updating a record.
+ #
+ # Article.new.published # => true
+ # Article.create.published # => true
+ #
+ # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope { where(:published_at => Time.now - 1.week) }
+ # end
+ #
+ # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
+ # macro, and it will be called when building the default scope.)
+ #
+ # If you use multiple <tt>default_scope</tt> declarations in your model then they will
+ # be merged together:
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope where(:published => true)
+ # default_scope where(:rating => 'G')
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
+ #
+ # This is also the case with inheritance and module includes where the parent or module
+ # defines a <tt>default_scope</tt> and the child or including class defines a second one.
+ #
+ # If you need to do more complex things with a default scope, you can alternatively
+ # define it as a class method:
+ #
+ # class Article < ActiveRecord::Base
+ # def self.default_scope
+ # # Should return a scope, you can call 'super' here etc.
+ # end
+ # end
+ def default_scope(scope = {})
+ scope = Proc.new if block_given?
+ self.default_scopes = default_scopes + [scope]
+ end
+
+ def build_default_scope #:nodoc:
+ if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods
+ evaluate_default_scope { default_scope }
+ elsif default_scopes.any?
+ 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
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
new file mode 100644
index 0000000000..0edc3f1dcc
--- /dev/null
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -0,0 +1,202 @@
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ # = Active Record Named \Scopes
+ module Scoping
+ module Named
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Returns an anonymous \scope.
+ #
+ # posts = Post.scoped
+ # posts.size # Fires "select count(*) from posts" and returns the count
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
+ #
+ # fruits = Fruit.scoped
+ # 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
+ # queries, where passing intermediate values (\scopes) around as first-class
+ # objects is convenient.
+ #
+ # You can define a \scope that applies to all finders using
+ # ActiveRecord::Base.default_scope.
+ def scoped(options = nil)
+ if options
+ scoped.apply_finder_options(options)
+ else
+ if current_scope
+ current_scope.clone
+ else
+ scope = relation.clone
+ scope.default_scoped = true
+ scope
+ end
+ 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>.
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :red, where(:color => 'red')
+ # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
+ # end
+ #
+ # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
+ # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
+ #
+ # Note that this is simply 'syntactic sugar' for defining an actual class method:
+ #
+ # class Shirt < ActiveRecord::Base
+ # def self.red
+ # where(:color => 'red')
+ # end
+ # end
+ #
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
+ # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
+ # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
+ # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
+ # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
+ # all behave as if Shirt.red really was an Array.
+ #
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
+ # all shirts that are both red and dry clean only.
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
+ # returns the number of garments for which these criteria obtain. Similarly with
+ # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
+ #
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
+ # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :shirts
+ # end
+ #
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
+ # only shirts.
+ #
+ # Named \scopes can also be procedural:
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :colored, lambda { |color| where(:color => color) }
+ # end
+ #
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
+ #
+ # On Ruby 1.9 you can use the 'stabby lambda' syntax:
+ #
+ # scope :colored, ->(color) { where(:color => color) }
+ #
+ # Note that scopes defined with \scope will be evaluated when they are defined, rather than
+ # when they are used. For example, the following would be incorrect:
+ #
+ # class Post < ActiveRecord::Base
+ # scope :recent, where('published_at >= ?', Time.current - 1.week)
+ # end
+ #
+ # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt>
+ # class was defined, and so the resultant SQL query would always be the same. The correct
+ # way to do this would be via a lambda, which will re-evaluate the scope each time
+ # it is called:
+ #
+ # class Post < ActiveRecord::Base
+ # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) }
+ # end
+ #
+ # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :red, where(:color => 'red') do
+ # def dom_id
+ # 'red_shirts'
+ # end
+ # end
+ # end
+ #
+ # Scopes can also be used while creating/building a record.
+ #
+ # class Article < ActiveRecord::Base
+ # scope :published, where(:published => true)
+ # end
+ #
+ # Article.published.new.published # => true
+ # Article.published.create.published # => true
+ #
+ # Class methods on your model are automatically available
+ # on scopes. Assuming the following setup:
+ #
+ # class Article < ActiveRecord::Base
+ # scope :published, where(:published => true)
+ # scope :featured, where(:featured => true)
+ #
+ # def self.latest_article
+ # order('published_at desc').first
+ # end
+ #
+ # def self.titles
+ # map(&:title)
+ # end
+ #
+ # end
+ #
+ # We are able to call the methods like this:
+ #
+ # Article.published.featured.latest_article
+ # Article.featured.titles
+
+ def scope(name, scope_options = {})
+ name = name.to_sym
+ valid_scope_name?(name)
+ extension = Module.new(&Proc.new) if block_given?
+
+ scope_proc = lambda do |*args|
+ options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options
+ options = scoped.apply_finder_options(options) if options.is_a?(Hash)
+
+ relation = scoped.merge(options)
+
+ extension ? relation.extending(extension) : relation
+ end
+
+ singleton_class.send(:redefine_method, name, &scope_proc)
+ end
+
+ protected
+
+ def valid_scope_name?(name)
+ if respond_to?(name, true)
+ logger.warn "Creating scope :#{name}. " \
+ "Overwriting existing method #{self.name}.#{name}."
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index c23514c465..41e3b92499 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -2,13 +2,13 @@ module ActiveRecord #:nodoc:
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
- include ActiveModel::Serializable::JSON
+ include ActiveModel::Serializers::JSON
def serializable_hash(options = nil)
options = options.try(:clone) || {}
- options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
- options[:except] |= Array.wrap(self.class.inheritance_column)
+ options[:except] = Array(options[:except]).map { |n| n.to_s }
+ options[:except] |= Array(self.class.inheritance_column)
super(options)
end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 2da836ef0c..7f1dba5095 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -1,9 +1,8 @@
-require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/hash/conversions'
module ActiveRecord #:nodoc:
module Serialization
- include ActiveModel::Serializable::XML
+ include ActiveModel::Serializers::Xml
# Builds an XML document to represent the model. Some configuration is
# available through +options+. However more complicated cases should
@@ -176,13 +175,13 @@ module ActiveRecord #:nodoc:
end
end
- class XmlSerializer < ActiveModel::Serializable::XML::Serializer #:nodoc:
+ class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
def initialize(*args)
super
- options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column)
+ options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column)
end
- class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc:
+ class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
type = if klass.serialized_attributes.key?(name)
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index e3bbd06f7e..ce43ae8066 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -51,11 +51,11 @@ module ActiveRecord
class SessionStore < ActionDispatch::Session::AbstractStore
module ClassMethods # :nodoc:
def marshal(data)
- ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
+ ::Base64.encode64(Marshal.dump(data)) if data
end
def unmarshal(data)
- Marshal.load(ActiveSupport::Base64.decode64(data)) if data
+ Marshal.load(::Base64.decode64(data)) if data
end
def drop_table!
@@ -116,7 +116,7 @@ module ActiveRecord
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
- class << self; remove_method :find_by_session_id; end
+ class << self; remove_possible_method :find_by_session_id; end
def self.find_by_session_id(session_id)
find :first, :conditions => {:session_id=>session_id}
@@ -169,11 +169,11 @@ module ActiveRecord
# are implemented as class methods that you may override. By default,
# marshaling data is
#
- # ActiveSupport::Base64.encode64(Marshal.dump(data))
+ # ::Base64.encode64(Marshal.dump(data))
#
# and unmarshaling data is
#
- # Marshal.load(ActiveSupport::Base64.decode64(data))
+ # Marshal.load(::Base64.decode64(data))
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index ffe9b08dce..64ecef2077 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -9,15 +9,12 @@ module ActiveRecord
cleanup_identity_map
end
- def cleanup_identity_map
- ActiveRecord::IdentityMap.clear
+ def teardown
+ ActiveRecord::SQLCounter.log.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
+ def cleanup_identity_map
+ ActiveRecord::IdentityMap.clear
end
def assert_date_from_db(expected, actual, message = nil)
@@ -57,17 +54,8 @@ module ActiveRecord
ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql
end
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
+ def sqlite3? connection
+ connection.class.name.split('::').last == "SQLite3Adapter"
end
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 0c760e9850..c717fdea47 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -33,7 +33,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :record_timestamps
+ config_attribute :record_timestamps, :instance_writer => true
self.record_timestamps = true
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index ae97a3f3ca..b492377d18 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -211,7 +211,7 @@ module ActiveRecord
def after_commit(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
- options[:if] = Array.wrap(options[:if])
+ options[:if] = Array(options[:if])
options[:if] << "transaction_include_action?(:#{options[:on]})"
end
set_callback(:commit, :after, *args, &block)
@@ -220,7 +220,7 @@ module ActiveRecord
def after_rollback(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
- options[:if] = Array.wrap(options[:if])
+ options[:if] = Array(options[:if])
options[:if] << "transaction_include_action?(:#{options[:on]})"
end
set_callback(:rollback, :after, *args, &block)
@@ -301,7 +301,7 @@ module ActiveRecord
protected
# Save the new record state and id of a record so it can be restored later if a transaction fails.
- def remember_transaction_record_state #:nodoc
+ def remember_transaction_record_state #:nodoc:
@_start_transaction_state ||= {}
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
unless @_start_transaction_state.include?(:new_record)
@@ -314,7 +314,7 @@ module ActiveRecord
end
# Clear the new record state and id of a record.
- def clear_transaction_record_state #:nodoc
+ def clear_transaction_record_state #:nodoc:
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
@@ -322,7 +322,7 @@ module ActiveRecord
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false) #:nodoc
+ def restore_transaction_record_state(force = false) #:nodoc:
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
if @_start_transaction_state[:level] < 1
@@ -341,12 +341,12 @@ module ActiveRecord
end
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
- def transaction_record_state(state) #:nodoc
+ def transaction_record_state(state) #:nodoc:
@_start_transaction_state[state] if defined?(@_start_transaction_state)
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_action?(action) #:nodoc
+ def transaction_include_action?(action) #:nodoc:
case action
when :create
transaction_record_state(:new_record)
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
new file mode 100644
index 0000000000..ddcb5f2a7a
--- /dev/null
+++ b/activerecord/lib/active_record/translation.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ module Translation
+ include ActiveModel::Translation
+
+ # Set the lookup ancestors for ActiveModel.
+ def lookup_ancestors #:nodoc:
+ klass = self
+ classes = [klass]
+ return classes if klass == ActiveRecord::Base
+
+ while klass != klass.base_class
+ classes << klass = klass.superclass
+ end
+ classes
+ end
+
+ # Set the i18n scope to overwrite ActiveModel.
+ def i18n_scope #:nodoc:
+ :activerecord
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7af0352a31..afce149da9 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,8 +2,9 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
- record.errors.add(attribute, :invalid, options.merge(:value => value))
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any?
+ record.errors.add(attribute, :invalid, options.merge(:value => value))
+ end
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 2e2ea8c42b..3a741ba600 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/array/wrap'
-
module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator
@@ -25,8 +23,13 @@ module ActiveRecord
relation = build_relation(finder_class, table, attribute, value)
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
- Array.wrap(options[:scope]).each do |scope_item|
+ Array(options[:scope]).each do |scope_item|
scope_value = record.send(scope_item)
+ reflection = record.class.reflect_on_association(scope_item)
+ if reflection
+ scope_value = record.send(reflection.foreign_key)
+ scope_item = reflection.foreign_key
+ end
relation = relation.and(table[scope_item].eq(scope_value))
end
@@ -53,14 +56,22 @@ module ActiveRecord
end
def build_relation(klass, table, attribute, value) #:nodoc:
- column = klass.columns_hash[attribute.to_s]
- value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text?
+ reflection = klass.reflect_on_association(attribute)
+ column = nil
+ if(reflection)
+ column = klass.columns_hash[reflection.foreign_key]
+ attribute = reflection.foreign_key
+ value = value.attributes[reflection.primary_key_column.name]
+ else
+ column = klass.columns_hash[attribute.to_s]
+ end
+ value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text?
if !options[:case_sensitive] && value && column.text?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
- value = klass.connection.case_sensitive_modifier(value)
+ value = klass.connection.case_sensitive_modifier(value) unless value.nil?
relation = table[attribute].eq(value)
end
@@ -81,7 +92,7 @@ module ActiveRecord
#
# class Person < ActiveRecord::Base
# validates_uniqueness_of :user_name, :scope => :account_id
- # end
+ # end
#
# 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.
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 838aa8fb1e..0c35adc11d 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module VERSION #:nodoc:
- MAJOR = 3
- MINOR = 2
+ MAJOR = 4
+ MINOR = 0
TINY = 0
PRE = "beta"
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index f6159deeeb..1509e34473 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -3,7 +3,7 @@ require 'rails/generators/active_record'
module ActiveRecord
module Generators
class MigrationGenerator < Base
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
def create_migration_file
set_local_assigns!
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
index ce8d7eed42..d084a00ed7 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -2,14 +2,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<%- if migration_action == 'add' -%>
def change
<% attributes.each do |attribute| -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- if attribute.has_index? -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end %>
<%- end -%>
end
<%- else -%>
def up
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
- <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %>
+ <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
+ <% if attribute.has_index? && migration_action == 'add' %>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <% end -%>
<%- end -%>
<%- end -%>
end
@@ -17,7 +23,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
def down
<% attributes.reverse.each do |attribute| -%>
<%- if migration_action -%>
- <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %>
+ <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
<%- end -%>
<%- end -%>
end
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index f7caa43ac8..99a022461e 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -3,7 +3,7 @@ require 'rails/generators/active_record'
module ActiveRecord
module Generators
class ModelGenerator < Base
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
check_class_collision
@@ -26,6 +26,10 @@ module ActiveRecord
template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke
end
+ def attributes_with_index
+ attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) }
+ end
+
hook_for :test_framework
protected
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 851930344a..3a3cf86d73 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
@@ -2,16 +2,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
def change
create_table :<%= table_name %> do |t|
<% attributes.each do |attribute| -%>
- t.<%= attribute.type %> :<%= attribute.name %>
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% if options[:timestamps] %>
t.timestamps
<% end -%>
end
-<% if options[:indexes] -%>
-<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
- add_index :<%= table_name %>, :<%= attribute.name %>_id
-<% end -%>
+<% attributes_with_index.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<% end -%>
end
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index f1023ed7ef..852fc0e26e 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,159 +1,162 @@
require "cases/helper"
-class AdapterTest < ActiveRecord::TestCase
- def setup
- @connection = ActiveRecord::Base.connection
- end
-
- def test_tables
- tables = @connection.tables
- assert tables.include?("accounts")
- assert tables.include?("authors")
- assert tables.include?("tasks")
- assert tables.include?("topics")
- end
+module ActiveRecord
+ class AdapterTest < ActiveRecord::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
- def test_table_exists?
- assert @connection.table_exists?("accounts")
- assert !@connection.table_exists?("nonexistingtable")
- end
+ def test_tables
+ tables = @connection.tables
+ assert tables.include?("accounts")
+ assert tables.include?("authors")
+ assert tables.include?("tasks")
+ assert tables.include?("topics")
+ end
- def test_indexes
- idx_name = "accounts_idx"
-
- if @connection.respond_to?(:indexes)
- indexes = @connection.indexes("accounts")
- assert indexes.empty?
-
- @connection.add_index :accounts, :firm_id, :name => idx_name
- indexes = @connection.indexes("accounts")
- assert_equal "accounts", indexes.first.table
- # OpenBase does not have the concept of a named index
- # Indexes are merely properties of columns.
- assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
- assert !indexes.first.unique
- assert_equal ["firm_id"], indexes.first.columns
- else
- warn "#{@connection.class} does not respond to #indexes"
+ def test_table_exists?
+ assert @connection.table_exists?("accounts")
+ assert !@connection.table_exists?("nonexistingtable")
+ assert !@connection.table_exists?(nil)
end
- ensure
- @connection.remove_index(:accounts, :name => idx_name) rescue nil
- end
+ def test_indexes
+ idx_name = "accounts_idx"
+
+ if @connection.respond_to?(:indexes)
+ indexes = @connection.indexes("accounts")
+ assert indexes.empty?
+
+ @connection.add_index :accounts, :firm_id, :name => idx_name
+ indexes = @connection.indexes("accounts")
+ assert_equal "accounts", indexes.first.table
+ # OpenBase does not have the concept of a named index
+ # Indexes are merely properties of columns.
+ assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
+ assert !indexes.first.unique
+ assert_equal ["firm_id"], indexes.first.columns
+ else
+ warn "#{@connection.class} does not respond to #indexes"
+ end
- def test_current_database
- if @connection.respond_to?(:current_database)
- assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database
+ ensure
+ @connection.remove_index(:accounts, :name => idx_name) rescue nil
end
- end
- if current_adapter?(:MysqlAdapter)
- def test_charset
- assert_not_nil @connection.charset
- assert_not_equal 'character_set_database', @connection.charset
- assert_equal @connection.show_variable('character_set_database'), @connection.charset
+ def test_current_database
+ if @connection.respond_to?(:current_database)
+ assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database
+ end
end
- def test_collation
- assert_not_nil @connection.collation
- assert_not_equal 'collation_database', @connection.collation
- assert_equal @connection.show_variable('collation_database'), @connection.collation
- end
+ if current_adapter?(:MysqlAdapter)
+ def test_charset
+ assert_not_nil @connection.charset
+ assert_not_equal 'character_set_database', @connection.charset
+ assert_equal @connection.show_variable('character_set_database'), @connection.charset
+ end
- def test_show_nonexistent_variable_returns_nil
- assert_nil @connection.show_variable('foo_bar_baz')
- end
+ def test_collation
+ assert_not_nil @connection.collation
+ assert_not_equal 'collation_database', @connection.collation
+ assert_equal @connection.show_variable('collation_database'), @connection.collation
+ end
- def test_not_specifying_database_name_for_cross_database_selects
- begin
- assert_nothing_raised do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
+ def test_show_nonexistent_variable_returns_nil
+ assert_nil @connection.show_variable('foo_bar_baz')
+ end
- 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"
- )
+ def test_not_specifying_database_name_for_cross_database_selects
+ begin
+ assert_nothing_raised do
+ ActiveRecord::Model.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
+
+ config = ARTest.connection_config
+ ActiveRecord::Model.connection.execute(
+ "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
+ "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
+ )
+ end
+ ensure
+ ActiveRecord::Model.establish_connection 'arunit'
end
- ensure
- ActiveRecord::Base.establish_connection 'arunit'
end
end
- end
- def test_table_alias
- def @connection.test_table_alias_length() 10; end
- class << @connection
- alias_method :old_table_alias_length, :table_alias_length
- alias_method :table_alias_length, :test_table_alias_length
- end
+ def test_table_alias
+ def @connection.test_table_alias_length() 10; end
+ class << @connection
+ alias_method :old_table_alias_length, :table_alias_length
+ alias_method :table_alias_length, :test_table_alias_length
+ end
- assert_equal 'posts', @connection.table_alias_for('posts')
- assert_equal 'posts_comm', @connection.table_alias_for('posts_comments')
- assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts')
+ assert_equal 'posts', @connection.table_alias_for('posts')
+ assert_equal 'posts_comm', @connection.table_alias_for('posts_comments')
+ assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts')
- class << @connection
- remove_method :table_alias_length
- alias_method :table_alias_length, :old_table_alias_length
+ class << @connection
+ remove_method :table_alias_length
+ alias_method :table_alias_length, :old_table_alias_length
+ end
end
- end
- # test resetting sequences in odd tables in postgreSQL
- if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
- require 'models/movie'
- require 'models/subscriber'
+ # test resetting sequences in odd tables in postgreSQL
+ if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
+ require 'models/movie'
+ require 'models/subscriber'
- def test_reset_empty_table_with_custom_pk
- Movie.delete_all
- Movie.connection.reset_pk_sequence! 'movies'
- assert_equal 1, Movie.create(:name => 'fight club').id
- end
+ def test_reset_empty_table_with_custom_pk
+ Movie.delete_all
+ Movie.connection.reset_pk_sequence! 'movies'
+ assert_equal 1, Movie.create(:name => 'fight club').id
+ end
- if ActiveRecord::Base.connection.adapter_name != "FrontBase"
- def test_reset_table_with_non_integer_pk
- Subscriber.delete_all
- Subscriber.connection.reset_pk_sequence! 'subscribers'
- sub = Subscriber.new(:name => 'robert drake')
- sub.id = 'bob drake'
- assert_nothing_raised { sub.save! }
+ if ActiveRecord::Base.connection.adapter_name != "FrontBase"
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! 'subscribers'
+ sub = Subscriber.new(:name => 'robert drake')
+ sub.id = 'bob drake'
+ assert_nothing_raised { sub.save! }
+ end
end
end
- end
- def test_uniqueness_violations_are_translated_to_specific_exception
- @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
- assert_raises(ActiveRecord::RecordNotUnique) do
+ def test_uniqueness_violations_are_translated_to_specific_exception
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
+ end
end
- end
- def test_foreign_key_violations_are_translated_to_specific_exception
- unless @connection.adapter_name == 'SQLite'
- assert_raises(ActiveRecord::InvalidForeignKey) 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)"
+ def test_foreign_key_violations_are_translated_to_specific_exception
+ unless @connection.adapter_name == 'SQLite'
+ assert_raises(ActiveRecord::InvalidForeignKey) 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
end
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)"
+ 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
- # 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
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 2a89430da9..fa2ba8d592 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -3,13 +3,13 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
- @connection = ActiveRecord::Base.connection
+ @connection = ActiveRecord::Model.connection
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_true
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true}))
- assert ActiveRecord::Base.connection.raw_connection.reconnect
+ ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => true}))
+ assert ActiveRecord::Model.connection.raw_connection.reconnect
end
end
@@ -25,8 +25,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase
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}))
- assert !ActiveRecord::Base.connection.raw_connection.reconnect
+ ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => false}))
+ assert !ActiveRecord::Model.connection.raw_connection.reconnect
end
end
@@ -114,7 +114,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
# Test that MySQL allows multiple results for stored procedures
if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
def test_multi_results
- rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
+ rows = ActiveRecord::Model.connection.select_rows('CALL ten();')
assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
end
@@ -123,11 +123,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
+ original_connection = ActiveRecord::Model.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Base.establish_connection(original_connection)
+ ActiveRecord::Model.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 146b77a95c..7fe2c02c04 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -17,11 +17,7 @@ module ActiveRecord
end
def test_client_encoding
- if "<3".respond_to?(:encoding)
- assert_equal Encoding::UTF_8, @conn.client_encoding
- else
- assert_equal 'utf8', @conn.client_encoding
- end
+ assert_equal Encoding::UTF_8, @conn.client_encoding
end
def test_exec_insert_number
@@ -41,13 +37,11 @@ module ActiveRecord
value = result.rows.last.last
- if "<3".respond_to?(:encoding)
- # FIXME: this should probably be inside the mysql AR adapter?
- value.force_encoding(@conn.client_encoding)
+ # FIXME: this should probably be inside the mysql AR adapter?
+ value.force_encoding(@conn.client_encoding)
- # The strings in this file are utf-8, so transcode to utf-8
- value.encode!(Encoding::UTF_8)
- end
+ # The strings in this file are utf-8, so transcode to utf-8
+ value.encode!(Encoding::UTF_8)
assert_equal str, value
end
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index 1aa034ed53..29f885c6e7 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
- set_table_name "#{db}.#{table}"
+ self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 26091c713b..8e2b9ca9a5 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -3,7 +3,7 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
- @connection = ActiveRecord::Base.connection
+ @connection = ActiveRecord::Model.connection
end
def test_no_automatic_reconnection_after_timeout
@@ -32,11 +32,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
+ original_connection = ActiveRecord::Model.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Base.establish_connection(original_connection)
+ ActiveRecord::Model.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 49514e1539..d5676bc522 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
- set_table_name "#{db}.#{table}"
+ self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 21b97b3b39..4baec749ff 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -2,6 +2,9 @@ require "cases/helper"
module ActiveRecord
class PostgresqlConnectionTest < ActiveRecord::TestCase
+ class NonExistentTable < ActiveRecord::Base
+ end
+
def setup
super
@connection = ActiveRecord::Base.connection
@@ -10,5 +13,17 @@ module ActiveRecord
def test_encoding
assert_not_nil @connection.encoding
end
+
+ # Ensure, we can set connection params using the example of Generic
+ # Query Optimizer (geqo). It is 'on' per default.
+ def test_connection_options
+ params = ActiveRecord::Base.connection_config.dup
+ params[:options] = "-c geqo=off"
+ NonExistentTable.establish_connection(params)
+
+ # Verify the connection param has been applied.
+ expect = NonExistentTable.connection.query('show geqo').first.first
+ assert_equal 'off', expect
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
new file mode 100644
index 0000000000..33bf4478cc
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -0,0 +1,89 @@
+require "cases/helper"
+
+class PostgresqlHstoreTest < ActiveRecord::TestCase
+ class Hstore < ActiveRecord::Base
+ self.table_name = 'hstores'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('hstores') do |t|
+ t.hstore 'tags'
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ return skip "do not test on PG without hstore"
+ end
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists hstores'
+ end
+
+ def test_column
+ column = Hstore.columns.find { |c| c.name == 'tags' }
+ assert column
+ assert_equal :hstore, column.type
+ end
+
+ def test_type_cast_hstore
+ column = Hstore.columns.find { |c| c.name == 'tags' }
+ assert column
+
+ data = "\"1\"=>\"2\""
+ hash = column.class.cast_hstore data
+ assert_equal({'1' => '2'}, hash)
+ assert_equal({'1' => '2'}, column.type_cast(data))
+ end
+
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.find :first
+ assert_equal({'1' => '2'}, x.tags)
+ end
+
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.find :first
+ assert_equal({'1' => '2', '2' => '3'}, x.tags)
+ end
+
+ def test_create
+ assert_cycle('a' => 'b', '1' => '2')
+ end
+
+ def test_quotes
+ assert_cycle('a' => 'b"ar', '1"foo' => '2')
+ end
+
+ def test_whitespace
+ assert_cycle('a b' => 'b ar', '1"foo' => '2')
+ end
+
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
+ end
+
+ def test_comma
+ assert_cycle('a, b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_arrow
+ assert_cycle('a=>b' => 'bar', '1"foo' => '2')
+ end
+
+ private
+ def assert_cycle hash
+ x = Hstore.create!(:tags => hash)
+ x.reload
+ assert_equal(hash, x.tags)
+
+ # make sure updates work
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 467e5d7b86..18670b4177 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -26,23 +26,23 @@ class SchemaTest < ActiveRecord::TestCase
PK_TABLE_NAME = 'table_with_pk'
class Thing1 < ActiveRecord::Base
- set_table_name "test_schema.things"
+ self.table_name = "test_schema.things"
end
class Thing2 < ActiveRecord::Base
- set_table_name "test_schema2.things"
+ self.table_name = "test_schema2.things"
end
class Thing3 < ActiveRecord::Base
- set_table_name 'test_schema."things.table"'
+ self.table_name = 'test_schema."things.table"'
end
class Thing4 < ActiveRecord::Base
- set_table_name 'test_schema."Things"'
+ self.table_name = 'test_schema."Things"'
end
class Thing5 < ActiveRecord::Base
- set_table_name 'things'
+ self.table_name = 'things'
end
def setup
@@ -68,11 +68,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_schema_change_with_prepared_stmt
+ altered = false
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
@connection.exec_query "alter table developers add column zomg int", 'sql', []
+ altered = true
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
ensure
- @connection.exec_query "alter table developers drop column if exists zomg", 'sql', []
+ # We are not using DROP COLUMN IF EXISTS because that syntax is only
+ # supported by pg 9.X
+ @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered
end
def test_table_exists?
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 5f08f79171..9e7b08ef34 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,3 +1,5 @@
+require 'cases/helper'
+
class PostgreSQLUtilsTest < ActiveSupport::TestCase
include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
index 303ba9245a..66e07b71a0 100644
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/view_test.rb
@@ -14,7 +14,7 @@ class ViewTest < ActiveRecord::TestCase
]
class ThingView < ActiveRecord::Base
- set_table_name 'test_schema.view_things'
+ self.table_name = 'test_schema.view_things'
end
def setup
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index e0152e7ccf..46da1b0a2b 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -70,9 +70,9 @@ module ActiveRecord
assert_equal bd.to_f, @conn.type_cast(bd, nil)
end
- def test_type_cast_unknown
+ def test_type_cast_unknown_should_raise_error
obj = Class.new.new
- assert_equal YAML.dump(obj), @conn.type_cast(obj, nil)
+ assert_raise(TypeError) { @conn.type_cast(obj, nil) }
end
def test_quoted_id
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 97b56d38d7..17bde6cb62 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -23,8 +23,6 @@ module ActiveRecord
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 ', '
@@ -144,8 +142,6 @@ module ActiveRecord
end
def test_quote_binary_column_escapes_it
- return unless "<3".respond_to?(:encode)
-
DualEncoding.connection.execute(<<-eosql)
CREATE TABLE dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
@@ -159,9 +155,7 @@ module ActiveRecord
assert_equal str, binary.data
ensure
- if "<3".respond_to?(:encode)
- DualEncoding.connection.drop_table('dual_encodings')
- end
+ DualEncoding.connection.drop_table('dual_encodings')
end
def test_execute
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index ff376a68d8..6733f3e889 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -61,7 +61,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_cascaded_eager_association_loading_with_duplicated_includes
- categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null")
+ categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null").references(:categorizations)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.all.size
@@ -69,7 +69,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
- categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null")
+ categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null").references(:posts)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.all.size
@@ -127,7 +127,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
silly.parent_id = 1
assert silly.save
- topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
+ topics = Topic.find(:all, :include => :replies, :order => ['topics.id', 'replies_topics.id'])
assert_no_queries do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index d75791cab9..7965bb404c 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -4,7 +4,7 @@ require 'models/tagging'
module Namespaced
class Post < ActiveRecord::Base
- set_table_name 'posts'
+ self.table_name = 'posts'
has_one :tagging, :as => :taggable, :class_name => 'Tagging'
end
end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 2cf9f89c3c..1e1958410c 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -6,7 +6,6 @@ require 'models/comment'
require 'models/category'
require 'models/categorization'
require 'models/tagging'
-require 'active_support/core_ext/array/random_access'
module Remembered
extend ActiveSupport::Concern
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index c6e451fc57..b79c69bbb5 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -61,7 +61,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_loading_conditions_with_or
- posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
+ posts = authors(:david).posts.find(
+ :all, :include => :comments, :references => :comments,
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'"
+ )
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
"expected to find only david's posts"
end
@@ -164,7 +167,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
car_post.categories << categories(:technology)
comment = car_post.comments.create!(:body => "hmm")
- categories = Category.find(:all, :conditions => ["posts.id=?", car_post.id],
+ categories = Category.find(:all, :conditions => { 'posts.id' => car_post.id },
:include => {:posts => :comments})
categories.each do |category|
assert_equal [comment], category.posts[0].comments
@@ -252,6 +255,50 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_nested_loading_through_has_one_association
+ aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts})
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order
+ aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.id')
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order_on_association
+ aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.id')
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order_on_nested_association
+ aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.id')
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions
+ aa = AuthorAddress.find(
+ author_addresses(:david_address).id, :include => {:author => :posts},
+ :conditions => "author_addresses.id > 0", :references => :author_addresses
+ )
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions_on_association
+ aa = AuthorAddress.find(
+ author_addresses(:david_address).id, :include => {:author => :posts},
+ :conditions => "authors.id > 0", :references => :authors
+ )
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions_on_nested_association
+ aa = AuthorAddress.find(
+ author_addresses(:david_address).id, :include => {:author => :posts},
+ :conditions => "posts.id > 0", :references => :posts
+ )
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
def test_eager_association_loading_with_belongs_to_and_foreign_keys
pets = Pet.find(:all, :include => :owner)
assert_equal 3, pets.length
@@ -297,7 +344,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
- Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4])
+ ActiveSupport::Deprecation.silence do
+ Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4])
+ end
end
end
@@ -316,7 +365,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4])
+ ActiveSupport::Deprecation.silence do
+ Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4])
+ end
end
end
@@ -329,7 +380,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- Comment.find(:all, :include => :post, :order => quoted_posts_id)
+ ActiveSupport::Deprecation.silence do
+ Comment.find(:all, :include => :post, :order => quoted_posts_id)
+ end
end
end
@@ -493,22 +546,27 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
- posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ posts = ActiveSupport::Deprecation.silence do
+ Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ end
assert_equal 2, posts.size
- count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ count = ActiveSupport::Deprecation.silence do
+ Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
+ end
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_and_high_offset
- posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' })
assert_equal 0, posts.size
end
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
assert_queries(1) do
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
- :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ])
+ :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ],
+ :references => [:authors, :comments])
assert_equal 0, posts.size
end
end
@@ -522,7 +580,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_count_eager_with_has_many_and_limit_and_high_offset
- posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
+ posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' })
assert_equal 0, posts
end
@@ -534,7 +592,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
author = authors(:david)
author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
- assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
+ assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null', :references => :comments)
end
def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional
@@ -573,6 +631,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
+ :references => :comments,
:limit => 2
)
assert_equal 2, posts.size
@@ -580,6 +639,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
+ :references => [:authors, :comments],
:limit => 2
)
assert_equal count, posts.size
@@ -589,7 +649,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts = nil
Post.send(:with_scope, :find => {
:include => :comments,
- :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
+ :references => :comments
}) do
posts = authors(:david).posts.find(:all, :limit => 2)
assert_equal 2, posts.size
@@ -597,7 +658,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
Post.send(:with_scope, :find => {
:include => [ :comments, :author ],
- :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
+ :references => [:authors, :comments]
}) do
count = Post.count(:limit => 2)
assert_equal count, posts.size
@@ -609,6 +671,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
+ :references => :comments,
:limit => 2
)
assert_equal 2, posts.size
@@ -616,6 +679,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
+ :references => [:authors, :comments],
:limit => 2
)
assert_equal count, posts.size
@@ -623,9 +687,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
- posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
+ posts_with_explicit_order = Post.find(
+ :all, :conditions => 'comments.id is not null', :references => :comments,
+ :include => :comments, :order => 'posts.id DESC', :limit => 2
+ )
posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do
- Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
+ Post.find(
+ :all, :conditions => 'comments.id is not null',
+ :references => :comments, :include => :comments, :limit => 2
+ )
end
assert_equal posts_with_explicit_order, posts_with_scoped_order
end
@@ -738,17 +808,49 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_limited_eager_with_order
- assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1)
- assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1)
+ assert_equal(
+ posts(:thinking, :sti_comments),
+ Post.find(
+ :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' },
+ :order => 'UPPER(posts.title)', :limit => 2, :offset => 1
+ )
+ )
+ assert_equal(
+ posts(:sti_post_and_comments, :sti_comments),
+ Post.find(
+ :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' },
+ :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1
+ )
+ )
end
def test_limited_eager_with_multiple_order_columns
- assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1)
- assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1)
+ assert_equal(
+ posts(:thinking, :sti_comments),
+ Post.find(
+ :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' },
+ :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1
+ )
+ )
+ assert_equal(
+ posts(:sti_post_and_comments, :sti_comments),
+ Post.find(
+ :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' },
+ :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1
+ )
+ )
end
def test_limited_eager_with_numeric_in_association
- assert_equal people(:david, :susan), Person.find(:all, :include => [:readers, :primary_contact, :number1_fan], :conditions => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0)
+ assert_equal(
+ people(:david, :susan),
+ Person.find(
+ :all, :include => [:readers, :primary_contact, :number1_fan],
+ :conditions => "number1_fans_people.first_name like 'M%'",
+ :references => :number1_fans_people,
+ :order => 'people.id', :limit => 2, :offset => 0
+ )
+ )
end
def test_preload_with_interpolation
@@ -863,11 +965,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_count_with_include
if current_adapter?(:SybaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15", :references => :comments)
elsif current_adapter?(:OpenBaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15")
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15", :references => :comments)
else
- assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15")
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15", :references => :comments)
end
end
@@ -878,7 +980,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_conditions_on_join_table_with_include_and_limit
- assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).size
+ assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => { 'developers_projects.access_level' => 1 }, :limit => 5).size
end
def test_order_on_join_table_with_include_and_limit
@@ -1060,4 +1162,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first
end
end
+
+ def test_deep_including_through_habtm
+ posts = Post.find(:all, :include => {:categories => :categorizations}, :order => "posts.id")
+ assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
+ assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
+ assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length }
+ end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index 8dc1423375..d7c489c2b5 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -36,11 +36,6 @@ 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
@@ -51,11 +46,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
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
@@ -71,6 +61,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
end
+ def test_proxy_association_after_scoped
+ post = posts(:welcome)
+ assert_equal post.association(:comments), post.comments.the_association
+ assert_equal post.association(:comments), post.comments.scoped.the_association
+ end
+
private
def extension_name(model)
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 34d90cc395..f457dfb9b3 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
@@ -23,7 +23,7 @@ require 'models/treaty'
require 'active_support/core_ext/string/conversions'
class ProjectWithAfterCreateHook < ActiveRecord::Base
- set_table_name 'projects'
+ self.table_name = 'projects'
has_and_belongs_to_many :developers,
:class_name => "DeveloperForProjectWithAfterCreateHook",
:join_table => "developers_projects",
@@ -39,7 +39,7 @@ class ProjectWithAfterCreateHook < ActiveRecord::Base
end
class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
has_and_belongs_to_many :projects,
:class_name => "ProjectWithAfterCreateHook",
:join_table => "developers_projects",
@@ -48,7 +48,7 @@ class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
end
class ProjectWithSymbolsForKeys < ActiveRecord::Base
- set_table_name 'projects'
+ self.table_name = 'projects'
has_and_belongs_to_many :developers,
:class_name => "DeveloperWithSymbolsForKeys",
:join_table => :developers_projects,
@@ -57,7 +57,7 @@ class ProjectWithSymbolsForKeys < ActiveRecord::Base
end
class DeveloperWithSymbolsForKeys < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
has_and_belongs_to_many :projects,
:class_name => "ProjectWithSymbolsForKeys",
:join_table => :developers_projects,
@@ -66,7 +66,7 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
end
class DeveloperWithCounterSQL < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
has_and_belongs_to_many :projects,
:class_name => "DeveloperWithCounterSQL",
:join_table => "developers_projects",
@@ -77,7 +77,7 @@ end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
- :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
+ :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
def setup_data_for_habtm_case
ActiveRecord::Base.connection.execute('delete from countries_treaties')
@@ -445,6 +445,26 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert david.projects(true).empty?
end
+ def test_destroy_associations_destroys_multiple_associations
+ george = parrots(:george)
+ assert !george.pirates.empty?
+ assert !george.treasures.empty?
+
+ assert_no_difference "Pirate.count" do
+ assert_no_difference "Treasure.count" do
+ george.destroy_associations
+ end
+ end
+
+ join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
+ assert join_records.empty?
+ assert george.pirates(true).empty?
+
+ join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
+ assert join_records.empty?
+ assert george.treasures(true).empty?
+ end
+
def test_deprecated_push_with_attributes_was_removed
jamis = developers(:jamis)
assert_raise(NoMethodError) do
@@ -659,7 +679,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
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
+ assert_equal(
+ 3,
+ Developer.find(
+ :all, :include => {:projects => :developers}, :references => :developers_projects_join,
+ :conditions => 'developers_projects_join.joined_on IS NOT NULL'
+ ).size
+ )
end
def test_join_with_group
@@ -669,7 +695,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
Project.columns.each { |c| group << "projects.#{c.name}" }
- assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size
+ assert_equal(
+ 3,
+ Developer.find(
+ :all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL',
+ :references => :developers_projects_join, :group => group.join(",")
+ ).size
+ )
end
def test_find_grouped
@@ -805,12 +837,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
# clear cache possibly created by other tests
david.projects.reset_column_information
- # One query for columns, one for primary key
- assert_queries(2) { david.projects.columns; david.projects.columns }
+ assert_queries(1) { david.projects.columns; david.projects.columns }
## and again to verify that reset_column_information clears the cache correctly
david.projects.reset_column_information
- assert_queries(2) { david.projects.columns; david.projects.columns }
+ assert_queries(1) { david.projects.columns; david.projects.columns }
end
def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index a60af7c046..f1a341437f 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -8,6 +8,7 @@ require 'models/reply'
require 'models/category'
require 'models/post'
require 'models/author'
+require 'models/essay'
require 'models/comment'
require 'models/person'
require 'models/reader'
@@ -56,12 +57,22 @@ class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestC
end
end
+class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
+ fixtures :authors, :posts, :comments
+
+ def test_should_generate_valid_sql
+ author = authors(:david)
+ # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
+ # if the reorder clauses are not correctly handled
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ end
+end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
- :people, :posts, :readers, :taggings, :cars
+ :people, :posts, :readers, :taggings, :cars, :essays
def setup
Client.destroyed_client_ids.clear
@@ -1390,6 +1401,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
end
+
+ def test_custom_primary_key_on_new_record_should_fetch_with_query
+ author = Author.new(:name => "David")
+ assert !author.essays.loaded?
+
+ assert_queries 1 do
+ assert_equal 1, author.essays.size
+ end
+
+ assert_equal author.essays, Essay.find_all_by_writer_id("David")
+
+ end
+
+ def test_has_many_custom_primary_key
+ david = authors(:david)
+ assert_equal david.essays, Essay.find_all_by_writer_id("David")
+ end
+
+ def test_blank_custom_primary_key_on_new_record_should_not_run_queries
+ author = Author.new
+ assert !author.essays.loaded?
+
+ assert_queries 0 do
+ assert_equal 0, author.essays.size
+ end
+ end
def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query
firm = companies(:first_firm)
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 7a6aba6a6b..4612bc2618 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -528,6 +528,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
end
+ def test_get_ids_for_has_many_through_with_conditions_should_not_preload
+ Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc))
+ ActiveRecord::Associations::Preloader.expects(:new).never
+ posts(:welcome).misc_tag_ids
+ end
+
def test_get_ids_for_loaded_associations
person = people(:michael)
person.posts(true)
@@ -841,7 +847,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
+ person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').references(:readers).includes(:posts).to_a.first
assert person.posts.loaded?, 'person.posts should be loaded'
assert_equal [], person.posts
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index e5e9ca6131..68a1e62328 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -67,7 +67,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_find_with_implicit_inner_joins_does_not_set_associations
authors = Author.joins(:posts).select('authors.*')
assert !authors.empty?, "expected authors to be non-empty"
- assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded"
+ assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
def test_count_honors_implicit_inner_joins
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 4ce8b85098..301755249c 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -362,7 +362,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
assert_raise ActiveRecord::EagerLoadPolymorphicError do
- tags(:general).taggings.find(:all, :include => :taggable, :conditions => 'bogus_table.column = 1')
+ tags(:general).taggings.find(:all, :include => :taggable, :references => :bogus_table, :conditions => 'bogus_table.column = 1')
end
end
@@ -733,7 +733,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
Post.find(post_id).update_column :type, class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
- klass.set_table_name 'posts'
+ klass.table_name = 'posts'
klass.send(association, association_name, :as => :taggable, :dependent => dependency)
klass.find(post_id)
end
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 530f5212a2..03d99d19f6 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -505,7 +505,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
- Author.where('tags.id = tags.id'),
+ Author.where('tags.id = tags.id').references(:tags),
[authors(:bob)], :misc_post_first_blue_tags
)
end
@@ -526,7 +526,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
- Author.where('tags.id = tags.id'),
+ Author.where('tags.id = tags.id').references(:tags),
[authors(:bob)], :misc_post_first_blue_tags_2
)
end
@@ -545,6 +545,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [organizations(:nsa)], organizations
end
+ def test_nested_has_many_through_should_not_be_autosaved
+ c = Categorization.new
+ c.author = authors(:david)
+ c.post_taggings.to_a
+ assert !c.post_taggings.empty?
+ c.save
+ assert !c.post_taggings.empty?
+ end
+
private
def assert_includes_and_joins_equal(query, expected, association)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index ffe2993e0f..017905e0ac 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/computer'
require 'models/developer'
require 'models/project'
require 'models/company'
@@ -28,7 +29,7 @@ class AssociationsTest < ActiveRecord::TestCase
molecule.electrons.create(:name => 'electron_1')
molecule.electrons.create(:name => 'electron_2')
- liquids = Liquid.includes(:molecules => :electrons).where('molecules.id is not null')
+ liquids = Liquid.includes(:molecules => :electrons).references(:molecules).where('molecules.id is not null')
assert_equal 1, liquids[0].molecules.length
end
@@ -128,6 +129,11 @@ class AssociationsTest < ActiveRecord::TestCase
end
end
+ def test_association_with_references
+ firm = companies(:first_firm)
+ assert_equal ['foo'], firm.association_with_references.scoped.references_values
+ end
+
end
class AssociationProxyTest < ActiveRecord::TestCase
@@ -273,3 +279,18 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
)
end
end
+
+class GeneratedMethodsTest < ActiveRecord::TestCase
+ fixtures :developers, :computers, :posts, :comments
+ def test_association_methods_override_attribute_methods_of_same_name
+ assert_equal(developers(:david), computers(:workstation).developer)
+ # this next line will fail if the attribute methods module is generated lazily
+ # after the association methods module is generated
+ assert_equal(developers(:david), computers(:workstation).developer)
+ assert_equal(developers(:david).id, computers(:workstation)[:developer])
+ end
+
+ def test_model_method_overrides_association_method
+ assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index e03ed33591..764305459d 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'active_support/core_ext/object/inclusion'
+require 'thread'
module ActiveRecord
module AttributeMethods
@@ -14,8 +15,18 @@ module ActiveRecord
def setup
@klass = Class.new do
+ def self.superclass; Base; end
+ def self.active_record_super; Base; end
+ def self.base_class; self; end
+
include ActiveRecord::AttributeMethods
- include ActiveRecord::AttributeMethods::Read
+
+ def self.define_attribute_methods
+ # Created in the inherited/included hook for "proper" ARs
+ @attribute_methods_mutex ||= Mutex.new
+
+ super
+ end
def self.column_names
%w{ one two three }
@@ -33,9 +44,6 @@ module ActiveRecord
[name, FakeColumn.new(name)]
}]
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 9300c57819..d6de668a17 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -53,6 +53,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert Boolean.find(b4.id).attribute_present?(:value)
end
+ def test_caching_nil_primary_key
+ klass = Class.new(Minimalistic)
+ klass.expects(:reset_primary_key).returns(nil).once
+ 2.times { klass.primary_key }
+ end
+
def test_attribute_keys_on_new_instance
t = Topic.new
assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
@@ -96,7 +102,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_respond_to?
topic = Topic.find(1)
assert_respond_to topic, "title"
- assert_respond_to topic, "_title"
assert_respond_to topic, "title?"
assert_respond_to topic, "title="
assert_respond_to topic, :title
@@ -113,9 +118,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
assert keyboard.respond_to?('key_number')
- assert keyboard.respond_to?('_key_number')
assert keyboard.respond_to?('id')
- assert keyboard.respond_to?('_id')
end
# Syck calls respond_to? before actually calling initialize
@@ -255,8 +258,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic.send(:write_attribute, :title, "Still another topic")
assert_equal "Still another topic", topic.title
- topic.send(:write_attribute, "title", "Still another topic: part 2")
+ topic[:title] = "Still another topic: part 2"
assert_equal "Still another topic: part 2", topic.title
+
+ topic.send(:write_attribute, "title", "Still another topic: part 3")
+ assert_equal "Still another topic: part 3", topic.title
+
+ topic["title"] = "Still another topic: part 4"
+ assert_equal "Still another topic: part 4", topic.title
end
def test_read_attribute
@@ -309,6 +318,39 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# puts ""
end
+ def test_overridden_write_attribute
+ topic = Topic.new
+ def topic.write_attribute(attr_name, value)
+ super(attr_name, value.downcase)
+ end
+
+ topic.send(:write_attribute, :title, "Yet another topic")
+ assert_equal "yet another topic", topic.title
+
+ topic[:title] = "Yet another topic: part 2"
+ assert_equal "yet another topic: part 2", topic.title
+
+ topic.send(:write_attribute, "title", "Yet another topic: part 3")
+ assert_equal "yet another topic: part 3", topic.title
+
+ topic["title"] = "Yet another topic: part 4"
+ assert_equal "yet another topic: part 4", topic.title
+ end
+
+ def test_overridden_read_attribute
+ topic = Topic.new
+ topic.title = "Stop changing the topic"
+ def topic.read_attribute(attr_name)
+ super(attr_name).upcase
+ end
+
+ assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, "title")
+ assert_equal "STOP CHANGING THE TOPIC", topic["title"]
+
+ assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, :title)
+ assert_equal "STOP CHANGING THE TOPIC", topic[:title]
+ end
+
def test_read_overridden_attribute
topic = Topic.new(:title => 'a')
def topic.title() 'b' end
@@ -507,6 +549,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_write_time_to_date_attributes
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new
+ record.last_read = Time.utc(2010, 1, 1, 10)
+ assert_equal Date.civil(2010, 1, 1), record.last_read
+ end
+ end
+
def test_time_attributes_are_retrieved_in_current_time_zone
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
@@ -542,6 +592,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_setting_time_zone_aware_read_attribute
+ utc_time = Time.utc(2008, 1, 1)
+ cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.create(:written_on => cst_time).reload
+ assert_equal utc_time, record[:written_on]
+ assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone
+ assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time
+ end
+ end
+
def test_setting_time_zone_aware_attribute_with_string
utc_time = Time.utc(2008, 1, 1)
(-11..13).each do |timezone_offset|
@@ -561,6 +622,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new
record.written_on = ' '
assert_nil record.written_on
+ assert_nil record[:written_on]
end
end
@@ -675,7 +737,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = Topic.new(:id => 5)
topic.id = 5
- topic.method(:id).owner.send(:remove_method, :id)
+ topic.method(:id).owner.send(:undef_method, :id)
assert_deprecated do
assert_equal 5, topic.id
@@ -684,19 +746,39 @@ class AttributeMethodsTest < ActiveRecord::TestCase
Topic.undefine_attribute_methods
end
+ def test_read_attribute_with_nil_should_not_asplode
+ assert_equal nil, Topic.new.read_attribute(nil)
+ end
+
+ # If B < A, and A defines an accessor for 'foo', we don't want to override
+ # that by defining a 'foo' method in the generated methods module for B.
+ # (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
+ def test_inherited_custom_accessors
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ self.abstract_class = true
+ def title; "omg"; end
+ def title=(val); self.author_name = val; end
+ end
+ subklass = Class.new(klass)
+ [klass, subklass].each(&:define_attribute_methods)
+
+ topic = subklass.find(1)
+ assert_equal "omg", topic.title
+
+ topic.title = "lol"
+ assert_equal "lol", topic.author_name
+ end
+
private
def cached_columns
- @cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name)
+ @cached_columns ||= time_related_columns_on_topic.map(&:name)
end
def time_related_columns_on_topic
Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) }
end
- def serialized_columns_on_topic
- Topic.columns.select { |c| Topic.serialized_attributes.include?(c.name) }
- end
-
def in_time_zone(zone)
old_zone = Time.zone
old_tz = ActiveRecord::Base.time_zone_aware_attributes
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 4ad2cdfc7e..1376810adf 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -347,6 +347,17 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
client.save!
assert_no_queries { assert_equal apple, client.firm }
end
+
+ def test_validation_does_not_validate_stale_association_target
+ valid_developer = Developer.create!(:name => "Dude", :salary => 50_000)
+ invalid_developer = Developer.new()
+
+ auditlog = AuditLog.new(:message => "foo")
+ auditlog.developer = invalid_developer
+ auditlog.developer_id = valid_developer.id
+
+ assert auditlog.valid?
+ end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
@@ -575,6 +586,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
@@ -897,6 +909,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
@@ -1020,6 +1033,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@ship = Ship.create(:name => 'Nights Dirty Lightning')
@pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
@@ -1268,6 +1282,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@association_name = :birds
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@@ -1282,6 +1297,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@association_name = :parrots
@habtm = true
@@ -1297,6 +1313,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@pirate.birds.create(:name => 'cookoo')
end
@@ -1313,6 +1330,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@pirate.create_ship(:name => 'titanic')
super
@@ -1335,6 +1353,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
@@ -1355,6 +1374,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
@@ -1377,6 +1397,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
self.use_transactional_fixtures = false unless supports_savepoints?
def setup
+ super
@pirate = Pirate.new
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 997c9e7e9d..0ea1b824e1 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -23,10 +23,18 @@ require 'models/edge'
require 'models/joke'
require 'models/bulb'
require 'models/bird'
+require 'models/teapot'
require 'rexml/document'
require 'active_support/core_ext/exception'
require 'bcrypt'
+class FirstAbstractClass < ActiveRecord::Base
+ self.abstract_class = true
+end
+class SecondAbstractClass < FirstAbstractClass
+ self.abstract_class = true
+end
+class Photo < SecondAbstractClass; end
class Category < ActiveRecord::Base; end
class Categorization < ActiveRecord::Base; end
class Smarts < ActiveRecord::Base; end
@@ -69,6 +77,16 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
+ def test_generated_methods_modules
+ modules = Computer.ancestors
+ assert modules.include?(Computer::GeneratedFeatureMethods)
+ assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
+ assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
+ "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
+ assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
+ assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1]))
+ end
+
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
@@ -169,6 +187,31 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def test_previously_changed
+ topic = Topic.find :first
+ topic.title = '<3<3<3'
+ assert_equal({}, topic.previous_changes)
+
+ topic.save!
+ expected = ["The First Topic", "<3<3<3"]
+ assert_equal(expected, topic.previous_changes['title'])
+ end
+
+ def test_previously_changed_dup
+ topic = Topic.find :first
+ topic.title = '<3<3<3'
+ topic.save!
+
+ t2 = topic.dup
+
+ assert_equal(topic.previous_changes, t2.previous_changes)
+
+ topic.title = "lolwut"
+ topic.save!
+
+ assert_not_equal(topic.previous_changes, t2.previous_changes)
+ end
+
def test_preserving_time_objects
assert_kind_of(
Time, Topic.find(1).bonus_time,
@@ -561,10 +604,12 @@ class BasicsTest < ActiveRecord::TestCase
weird = Weird.create('a$b' => 'value')
weird.reload
assert_equal 'value', weird.send('a$b')
+ assert_equal 'value', weird.read_attribute('a$b')
weird.update_column('a$b', 'value2')
weird.reload
assert_equal 'value2', weird.send('a$b')
+ assert_equal 'value2', weird.read_attribute('a$b')
end
def test_multiparameter_attributes_on_date
@@ -644,7 +689,7 @@ class BasicsTest < ActiveRecord::TestCase
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
end
def test_multiparameter_attributes_on_time_with_no_date
@@ -893,7 +938,7 @@ class BasicsTest < ActiveRecord::TestCase
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
end
def test_boolean
@@ -947,10 +992,9 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "b", duped_topic.title
# test if the attribute values have been duped
- topic.title = {"a" => "b"}
duped_topic = topic.dup
- duped_topic.title["a"] = "c"
- assert_equal "b", topic.title["a"]
+ duped_topic.title.replace "c"
+ assert_equal "a", topic.title
# test if attributes set as part of after_initialize are duped correctly
assert_equal topic.author_email_address, duped_topic.author_email_address
@@ -961,8 +1005,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_not_equal duped_topic.id, topic.id
duped_topic.reload
- # FIXME: I think this is poor behavior, and will fix it with #5686
- assert_equal({'a' => 'c'}.to_yaml, duped_topic.title)
+ assert_equal("c", duped_topic.title)
end
def test_dup_with_aggregate_of_same_name_as_attribute
@@ -1185,19 +1228,6 @@ class BasicsTest < ActiveRecord::TestCase
assert(auto.id > 0)
end
- def quote_column_name(name)
- "<#{name}>"
- end
-
- def test_quote_keys
- ar = AutoId.new
- source = {"foo" => "bar", "baz" => "quux"}
- actual = ar.send(:quote_columns, self, source)
- inverted = actual.invert
- assert_equal("<foo>", inverted["bar"])
- assert_equal("<baz>", inverted["quux"])
- end
-
def test_sql_injection_via_find
assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
Topic.find("123456 OR id > 0")
@@ -1235,6 +1265,27 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
+ def test_serialized_attribute_in_base_class
+ Topic.serialize("content", Hash)
+
+ hash = { 'content1' => 'value1', 'content2' => 'value2' }
+ important_topic = ImportantTopic.create("content" => hash)
+ assert_equal(hash, important_topic.content)
+
+ important_topic.reload
+ assert_equal(hash, important_topic.content)
+ end
+
+ def test_serialized_attribute_declared_in_subclass
+ hash = { 'important1' => 'value1', 'important2' => 'value2' }
+ important_topic = ImportantTopic.create("important" => hash)
+ assert_equal(hash, important_topic.important)
+
+ important_topic.reload
+ assert_equal(hash, important_topic.important)
+ assert_equal(hash, important_topic.read_attribute(:important))
+ end
+
def test_serialized_time_attribute
myobj = Time.local(2008,1,1,1,0)
topic = Topic.create("content" => myobj).reload
@@ -1359,22 +1410,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal author_name, Topic.find(topic.id).author_name
end
- if RUBY_VERSION < '1.9'
- def test_quote_chars
- with_kcode('UTF8') do
- str = 'The Narrator'
- topic = Topic.create(:author_name => str)
- assert_equal str, topic.author_name
-
- assert_kind_of ActiveSupport::Multibyte.proxy_class, str.mb_chars
- topic = Topic.find_by_author_name(str.mb_chars)
-
- assert_kind_of Topic, topic
- assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars"
- end
- end
- end
-
def test_toggle_attribute
assert !topics(:first).approved?
topics(:first).toggle!(:approved)
@@ -1401,37 +1436,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal dev, dev.reload
end
- def test_define_attr_method_with_value
- k = Class.new( ActiveRecord::Base )
- k.send(:define_attr_method, :table_name, "foo")
- assert_equal "foo", k.table_name
- end
-
- def test_define_attr_method_with_block
- k = Class.new( ActiveRecord::Base ) do
- class << self
- attr_accessor :foo_key
- end
- end
- k.foo_key = "id"
- k.send(:define_attr_method, :foo_key) { "sys_" + original_foo_key }
- assert_equal "sys_id", k.foo_key
- end
-
- def test_set_table_name_with_value
- k = Class.new( ActiveRecord::Base )
- k.table_name = "foo"
- assert_equal "foo", k.table_name
- k.set_table_name "bar"
- assert_equal "bar", k.table_name
- end
-
def test_switching_between_table_name
assert_difference("GoodJoke.count") do
- Joke.set_table_name "cold_jokes"
+ Joke.table_name = "cold_jokes"
Joke.create
- Joke.set_table_name "funny_jokes"
+ Joke.table_name = "funny_jokes"
Joke.create
end
end
@@ -1439,48 +1449,30 @@ class BasicsTest < ActiveRecord::TestCase
def test_quoted_table_name_after_set_table_name
klass = Class.new(ActiveRecord::Base)
- klass.set_table_name "foo"
+ klass.table_name = "foo"
assert_equal "foo", klass.table_name
assert_equal klass.connection.quote_table_name("foo"), klass.quoted_table_name
- klass.set_table_name "bar"
+ klass.table_name = "bar"
assert_equal "bar", klass.table_name
assert_equal klass.connection.quote_table_name("bar"), klass.quoted_table_name
end
- def test_set_table_name_with_block
- k = Class.new( ActiveRecord::Base )
- k.set_table_name { "ks" }
- assert_equal "ks", k.table_name
- end
-
- def test_set_primary_key_with_value
- k = Class.new( ActiveRecord::Base )
- k.primary_key = "foo"
- assert_equal "foo", k.primary_key
- k.set_primary_key "bar"
- assert_equal "bar", k.primary_key
- end
-
- def test_set_primary_key_with_block
+ def test_set_table_name_with_inheritance
k = Class.new( ActiveRecord::Base )
- k.primary_key = 'id'
- k.set_primary_key { "sys_" + original_primary_key }
- assert_equal "sys_id", k.primary_key
+ def k.name; "Foo"; end
+ def k.table_name; super + "ks"; end
+ assert_equal "foosks", k.table_name
end
- def test_set_inheritance_column_with_value
- k = Class.new( ActiveRecord::Base )
- k.inheritance_column = "foo"
- assert_equal "foo", k.inheritance_column
- k.set_inheritance_column "bar"
- assert_equal "bar", k.inheritance_column
- end
-
- def test_set_inheritance_column_with_block
- k = Class.new( ActiveRecord::Base )
- k.set_inheritance_column { original_inheritance_column + "_id" }
- assert_equal "type_id", k.inheritance_column
+ def test_sequence_name_with_abstract_class
+ ak = Class.new(ActiveRecord::Base)
+ ak.abstract_class = true
+ k = Class.new(ak)
+ k.table_name = "projects"
+ orig_name = k.sequence_name
+ return skip "sequences not supported by db" unless orig_name
+ assert_equal k.reset_sequence_name, orig_name
end
def test_count_with_join
@@ -1673,10 +1665,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_descends_from_active_record
- # Tries to call Object.abstract_class?
- assert_raise(NoMethodError) do
- ActiveRecord::Base.descends_from_active_record?
- end
+ assert !ActiveRecord::Base.descends_from_active_record?
# Abstract subclass of AR::Base.
assert LoosePerson.descends_from_active_record?
@@ -1699,6 +1688,10 @@ class BasicsTest < ActiveRecord::TestCase
# Concrete subclasses an abstract class which has a type column.
assert !SubStiPost.descends_from_active_record?
+
+ assert Teapot.descends_from_active_record?
+ assert !OtherTeapot.descends_from_active_record?
+ assert CoolTeapot.descends_from_active_record?
end
def test_find_on_abstract_base_class_doesnt_use_type_condition
@@ -1732,7 +1725,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_inspect_instance
topic = topics(:first)
- assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
end
def test_inspect_new_instance
@@ -1772,7 +1765,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_silence_sets_log_level_to_error_in_block
original_logger = ActiveRecord::Base.logger
log = StringIO.new
- ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::DEBUG
ActiveRecord::Base.silence do
ActiveRecord::Base.logger.warn "warn"
@@ -1786,7 +1779,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_silence_sets_log_level_back_to_level_before_yield
original_logger = ActiveRecord::Base.logger
log = StringIO.new
- ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::WARN
ActiveRecord::Base.silence do
end
@@ -1798,7 +1791,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_benchmark_with_log_level
original_logger = ActiveRecord::Base.logger
log = StringIO.new
- ActiveRecord::Base.logger = Logger.new(log)
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::WARN
ActiveRecord::Base.benchmark("Debug Topic Count", :level => :debug) { Topic.count }
ActiveRecord::Base.benchmark("Warn Topic Count", :level => :warn) { Topic.count }
@@ -1813,10 +1806,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_benchmark_with_use_silence
original_logger = ActiveRecord::Base.logger
log = StringIO.new
- ActiveRecord::Base.logger = Logger.new(log)
- ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => true) { ActiveRecord::Base.logger.debug "Loud" }
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" }
- assert_no_match(/Loud/, log.string)
assert_match(/Quiet/, log.string)
ensure
ActiveRecord::Base.logger = original_logger
@@ -1848,9 +1839,9 @@ class BasicsTest < ActiveRecord::TestCase
def test_clear_cache!
# preheat cache
- c1 = Post.columns
+ c1 = Post.connection.schema_cache.columns['posts']
ActiveRecord::Base.clear_cache!
- c2 = Post.columns
+ c2 = Post.connection.schema_cache.columns['posts']
assert_not_equal c1, c2
end
@@ -1867,11 +1858,6 @@ 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)
marshalled = Marshal.dump(expected)
actual = Marshal.load(marshalled)
@@ -1880,11 +1866,6 @@ class BasicsTest < ActiveRecord::TestCase
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)
@@ -1892,11 +1873,6 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -1915,7 +1891,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [], NonExistentTable.attribute_names
end
- def test_attribtue_names_on_abstract_class
+ def test_attribute_names_on_abstract_class
assert_equal [], AbstractCompany.attribute_names
end
@@ -1949,4 +1925,17 @@ class BasicsTest < ActiveRecord::TestCase
Bird.stubs(:scoped).returns(mock(:uniq => scope))
assert_equal scope, Bird.uniq
end
+
+ def test_active_record_super
+ assert_equal ActiveRecord::Model, ActiveRecord::Base.active_record_super
+ assert_equal ActiveRecord::Base, Topic.active_record_super
+ assert_equal Topic, ImportantTopic.active_record_super
+ assert_equal ActiveRecord::Model, Teapot.active_record_super
+ assert_equal Teapot, OtherTeapot.active_record_super
+ assert_equal ActiveRecord::Model, CoolTeapot.active_record_super
+ end
+
+ def test_table_name_with_2_abstract_subclasses
+ assert_equal "photos", Photo.table_name
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 06c14cb108..f97aade311 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -12,7 +12,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
def test_mixed_encoding
str = "\x80"
- str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
+ str.force_encoding('ASCII-8BIT')
binary = Binary.new :name => 'いただきます!', :data => str
binary.save!
@@ -23,7 +23,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
# Mysql adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
- name.force_encoding('UTF-8') if name.respond_to?(:force_encoding)
+ name.force_encoding('UTF-8')
end
assert_equal 'いただきます!', name
end
@@ -33,7 +33,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
FIXTURES.each do |filename|
data = File.read(ASSETS_ROOT + "/#{filename}")
- data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
+ data.force_encoding('ASCII-8BIT')
data.freeze
bin = Binary.new(:data => data)
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index c38814713a..7c9ebf528e 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/company'
+require "models/contract"
require 'models/topic'
require 'models/edge'
require 'models/club'
@@ -48,11 +49,11 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_get_maximum_of_field_with_include
- assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :conditions => "companies.name != 'Summit'")
+ assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'")
end
def test_should_get_maximum_of_field_with_scoped_include
- Account.send :with_scope, :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
+ Account.send :with_scope, :find => { :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'" } do
assert_equal 55, Account.maximum(:credit_limit)
end
end
@@ -269,7 +270,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_not_modify_options_when_using_includes
- options = {:conditions => 'companies.id > 1', :include => :firm}
+ options = {:conditions => 'companies.id > 1', :include => :firm, :references => :companies}
options_copy = options.dup
Account.count(:all, options)
@@ -446,4 +447,35 @@ class CalculationsTest < ActiveRecord::TestCase
distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true]
assert_equal distinct_authors_for_approved_count, 2
end
+
+ def test_pluck
+ assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
+ end
+
+ def test_pluck_type_cast
+ topic = topics(:first)
+ relation = Topic.where(:id => topic.id)
+ assert_equal [ topic.approved ], relation.pluck(:approved)
+ assert_equal [ topic.last_read ], relation.pluck(:last_read)
+ assert_equal [ topic.written_on ], relation.pluck(:written_on)
+ end
+
+ def test_pluck_and_uniq
+ assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit)
+ end
+
+ def test_pluck_in_relation
+ company = Company.first
+ contract = company.contracts.create!
+ assert_equal [contract.id], company.contracts.pluck(:id)
+ end
+
+ def test_pluck_with_serialization
+ t = Topic.create!(:content => { :foo => :bar })
+ assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content)
+ end
+
+ def test_pluck_with_qualified_column_name
+ assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id")
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 7f4d25790b..7690769226 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class CallbackDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
class << self
def callback_string(callback_method)
@@ -48,7 +48,7 @@ class CallbackDeveloperWithFalseValidation < CallbackDeveloper
end
class ParentDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
attr_accessor :after_save_called
before_validation {|record| record.after_save_called = true}
end
@@ -58,7 +58,7 @@ class ChildDeveloper < ParentDeveloper
end
class RecursiveCallbackDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
before_save :on_before_save
after_save :on_after_save
@@ -79,7 +79,7 @@ class RecursiveCallbackDeveloper < ActiveRecord::Base
end
class ImmutableDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
validates_inclusion_of :salary, :in => 50000..200000
@@ -98,7 +98,7 @@ class ImmutableDeveloper < ActiveRecord::Base
end
class ImmutableMethodDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
validates_inclusion_of :salary, :in => 50000..200000
@@ -118,7 +118,7 @@ class ImmutableMethodDeveloper < ActiveRecord::Base
end
class OnCallbacksDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
before_validation { history << :before_validation }
before_validation(:on => :create){ history << :before_validation_on_create }
@@ -138,7 +138,7 @@ class OnCallbacksDeveloper < ActiveRecord::Base
end
class CallbackCancellationDeveloper < ActiveRecord::Base
- set_table_name 'developers'
+ self.table_name = 'developers'
attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called
attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
new file mode 100644
index 0000000000..ccc57cb876
--- /dev/null
+++ b/activerecord/test/cases/column_test.rb
@@ -0,0 +1,29 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ColumnTest < ActiveRecord::TestCase
+ def test_type_cast_boolean
+ column = Column.new("field", nil, "boolean")
+ assert column.type_cast(true)
+ assert column.type_cast(1)
+ assert column.type_cast('1')
+ assert column.type_cast('t')
+ assert column.type_cast('T')
+ assert column.type_cast('true')
+ assert column.type_cast('TRUE')
+ assert column.type_cast('on')
+ assert column.type_cast('ON')
+ assert !column.type_cast(false)
+ assert !column.type_cast(0)
+ assert !column.type_cast('0')
+ assert !column.type_cast('f')
+ assert !column.type_cast('F')
+ assert !column.type_cast('false')
+ assert !column.type_cast('FALSE')
+ assert !column.type_cast('off')
+ assert !column.type_cast('OFF')
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
new file mode 100644
index 0000000000..7dc6e8afcb
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
@@ -0,0 +1,54 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapterTest < ActiveRecord::TestCase
+ attr_reader :adapter
+
+ def setup
+ @adapter = AbstractAdapter.new nil, nil
+ end
+
+ def test_in_use?
+ # FIXME: change to refute in Rails 4.0 / mt
+ assert !adapter.in_use?, 'adapter is not in use'
+ assert adapter.lease, 'lease adapter'
+ assert adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_lease_twice
+ assert adapter.lease, 'should lease adapter'
+ assert !adapter.lease, 'should not lease adapter'
+ end
+
+ def test_last_use
+ assert !adapter.last_use
+ adapter.lease
+ assert adapter.last_use
+ end
+
+ def test_expire_mutates_in_use
+ assert adapter.lease, 'lease adapter'
+ assert adapter.in_use?, 'adapter is in use'
+ adapter.expire
+ assert !adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_close
+ pool = ConnectionPool.new(ConnectionSpecification.new({}, nil))
+ pool.connections << adapter
+ adapter.pool = pool
+
+ # Make sure the pool marks the connection in use
+ assert_equal adapter, pool.connection
+ assert adapter.in_use?
+
+ # Close should put the adapter back in the pool
+ adapter.close
+ assert !adapter.in_use?
+
+ assert_equal adapter, pool.connection
+ end
+ end
+ 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 bd0d161838..dc99ac665c 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -8,6 +8,9 @@ module ActiveRecord
@handler.establish_connection 'america', Base.connection_pool.spec
@klass = Class.new do
def self.name; 'america'; end
+ class << self
+ alias active_record_super superclass
+ end
end
@subklass = Class.new(@klass) do
def self.name; 'north america'; end
@@ -40,8 +43,6 @@ module ActiveRecord
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),
diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb
new file mode 100644
index 0000000000..ea2196cda2
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb
@@ -0,0 +1,12 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionSpecificationTest < ActiveRecord::TestCase
+ def test_dup_deep_copy_config
+ spec = ConnectionSpecification.new({ :a => :b }, "bar")
+ assert_not_equal(spec.config.object_id, spec.dup.config.object_id)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 79e842f5e1..42e39d534c 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -6,12 +6,6 @@ module ActiveRecord
def setup
connection = ActiveRecord::Base.connection
@cache = SchemaCache.new connection
-
- if in_memory_db?
- connection.create_table :posts do |t|
- t.integer :cololumn
- end
- end
end
def test_primary_key
@@ -19,16 +13,7 @@ module ActiveRecord
end
def test_primary_key_for_non_existent_table
- assert_equal 'id', @cache.primary_keys['omgponies']
- end
-
- def test_primary_key_is_set_on_columns
- posts_columns = @cache.columns_hash['posts']
- assert posts_columns['id'].primary
-
- (posts_columns.keys - ['id']).each do |key|
- assert !posts_columns[key].primary
- end
+ assert_nil @cache.primary_keys['omgponies']
end
def test_caches_columns
@@ -41,14 +26,18 @@ module ActiveRecord
assert_equal columns_hash, @cache.columns_hash['posts']
end
- def test_clearing_column_cache
+ def test_clearing
@cache.columns['posts']
@cache.columns_hash['posts']
+ @cache.tables['posts']
+ @cache.primary_keys['posts']
@cache.clear!
assert_equal 0, @cache.columns.size
assert_equal 0, @cache.columns_hash.size
+ assert_equal 0, @cache.tables.size
+ assert_equal 0, @cache.primary_keys.size
end
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index f554ceef35..496cd78136 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "rack"
module ActiveRecord
module ConnectionAdapters
@@ -25,40 +26,6 @@ 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)
@@ -114,9 +81,10 @@ module ActiveRecord
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"
+ app = lambda { |_| [200, {}, body] }
+ response_body = ConnectionManagement.new(app).call(@env)[2]
+ assert response_body.respond_to?(:to_path)
+ assert_equal response_body.to_path, "/path"
end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 1550fa5530..2c69bfde5b 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -4,6 +4,8 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionPoolTest < ActiveRecord::TestCase
def setup
+ super
+
# Keep a duplicate pool so we do not bother others
@pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
@@ -18,36 +20,76 @@ module ActiveRecord
end
end
- def test_active_connection?
- assert !@pool.active_connection?
- assert @pool.connection
- assert @pool.active_connection?
- @pool.release_connection
- assert !@pool.active_connection?
+ def teardown
+ super
+ @pool.disconnect!
end
- def test_clear_stale_cached_connections!
- pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
+ def test_full_pool_exception
+ assert_raises(PoolFullError) do
+ (@pool.size + 1).times do
+ @pool.checkout
+ end
+ end
+ end
- threads = [
- Thread.new { pool.connection },
- Thread.new { pool.connection }]
+ def test_reap_and_active
+ @pool.checkout
+ @pool.checkout
+ @pool.checkout
+ @pool.timeout = 0
- threads.map { |t| t.join }
+ connections = @pool.connections.dup
- pool.extend Module.new {
- attr_accessor :checkins
- def checkin conn
- @checkins << conn
- conn.object_id
- end
- }
- pool.checkins = []
+ @pool.reap
+
+ assert_equal connections.length, @pool.connections.length
+ end
+
+ def test_reap_inactive
+ @pool.checkout
+ @pool.checkout
+ @pool.checkout
+ @pool.timeout = 0
+
+ connections = @pool.connections.dup
+ connections.each do |conn|
+ conn.extend(Module.new { def active?; false; end; })
+ end
+
+ @pool.reap
- cleared_threads = pool.clear_stale_cached_connections!
- assert((cleared_threads - threads.map { |x| x.object_id }).empty?,
- "threads should have been removed")
- assert_equal pool.checkins.length, threads.length
+ assert_equal 0, @pool.connections.length
+ ensure
+ connections.each(&:close)
+ end
+
+ def test_remove_connection
+ conn = @pool.checkout
+ assert conn.in_use?
+
+ length = @pool.connections.length
+ @pool.remove conn
+ assert conn.in_use?
+ assert_equal(length - 1, @pool.connections.length)
+ ensure
+ conn.close
+ end
+
+ def test_remove_connection_for_thread
+ conn = @pool.connection
+ @pool.remove conn
+ assert_not_equal(conn, @pool.connection)
+ ensure
+ conn.close if conn
+ end
+
+ def test_active_connection?
+ assert !@pool.active_connection?
+ assert @pool.connection
+ assert @pool.active_connection?
+ @pool.release_connection
+ assert !@pool.active_connection?
end
def test_checkout_behaviour
@@ -59,24 +101,16 @@ module ActiveRecord
threads << Thread.new(i) do |pool_count|
connection = pool.connection
assert_not_nil connection
+ connection.close
end
end
- threads.each {|t| t.join}
+ threads.each(&:join)
Thread.new do
- threads.each do |t|
- thread_ids = pool.instance_variable_get(:@reserved_connections).keys
- assert thread_ids.include?(t.object_id)
- end
-
- pool.connection
- threads.each do |t|
- thread_ids = pool.instance_variable_get(:@reserved_connections).keys
- assert !thread_ids.include?(t.object_id)
- end
- end.join()
-
+ assert pool.connection
+ pool.connection.close
+ end.join
end
def test_automatic_reconnect=
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
new file mode 100644
index 0000000000..e6cb1b9521
--- /dev/null
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -0,0 +1,41 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionSpecification
+ class ResolverTest < ActiveRecord::TestCase
+ def resolve(spec)
+ Resolver.new(spec, {}).spec.config
+ end
+
+ def test_url_host_no_db
+ spec = resolve 'mysql://foo?encoding=utf8'
+ assert_equal({
+ :adapter => "mysql",
+ :database => "",
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+
+ def test_url_host_db
+ spec = resolve 'mysql://foo/bar?encoding=utf8'
+ assert_equal({
+ :adapter => "mysql",
+ :database => "bar",
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+
+ def test_url_port
+ spec = resolve 'mysql://foo:123?encoding=utf8'
+ assert_equal({
+ :adapter => "mysql",
+ :database => "",
+ :port => 123,
+ :host => "foo",
+ :encoding => "utf8" }, spec)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 3ed96a3ec8..cd3d19e783 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -6,9 +6,11 @@ require 'models/engine'
require 'models/reply'
require 'models/category'
require 'models/categorization'
+require 'models/dog'
+require 'models/dog_lover'
class CounterCacheTest < ActiveRecord::TestCase
- fixtures :topics, :categories, :categorizations, :cars
+ fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers
class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
@@ -61,7 +63,7 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
- test "reset counter should with belongs_to which has class_name" do
+ test "reset counter with belongs_to which has class_name" do
car = cars(:honda)
assert_nothing_raised do
Car.reset_counters(car.id, :engines)
@@ -71,6 +73,20 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test "reset the right counter if two have the same class_name" do
+ david = dog_lovers(:david)
+
+ DogLover.increment_counter(:bred_dogs_count, david.id)
+ DogLover.increment_counter(:trained_dogs_count, david.id)
+
+ assert_difference 'david.reload.bred_dogs_count', -1 do
+ DogLover.reset_counters(david.id, :bred_dogs)
+ end
+ assert_difference 'david.reload.trained_dogs_count', -1 do
+ DogLover.reset_counters(david.id, :trained_dogs)
+ end
+ end
+
test "update counter with initial null value" do
category = categories(:general)
assert_equal 2, category.categorizations.count
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 0236f9b0a1..9705a11387 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -99,5 +99,13 @@ module ActiveRecord
assert_not_nil new_topic.created_at
end
+ def test_dup_after_initialize_callbacks
+ topic = Topic.new
+ assert Topic.after_initialize_called
+ Topic.after_initialize_called = false
+ topic.dup
+ assert Topic.after_initialize_called
+ end
+
end
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
new file mode 100644
index 0000000000..6ae6f83446
--- /dev/null
+++ b/activerecord/test/cases/explain_test.rb
@@ -0,0 +1,101 @@
+require 'cases/helper'
+require 'models/car'
+require 'active_support/core_ext/string/strip'
+
+if ActiveRecord::Base.connection.supports_explain?
+ class ExplainTest < ActiveRecord::TestCase
+ fixtures :cars
+
+ def base
+ ActiveRecord::Base
+ end
+
+ def connection
+ base.connection
+ end
+
+ def test_logging_query_plan
+ base.logger.expects(:warn).with do |message|
+ message.starts_with?('EXPLAIN for:')
+ end
+
+ with_threshold(0) do
+ Car.where(:name => 'honda').all
+ end
+ end
+
+ def test_collect_queries_for_explain
+ base.auto_explain_threshold_in_seconds = nil
+ queries = Thread.current[:available_queries_for_explain] = []
+
+ with_threshold(0) do
+ Car.where(:name => 'honda').all
+ end
+
+ sql, binds = queries[0]
+ assert_match "SELECT", sql
+ assert_match "honda", sql
+ assert_equal [], binds
+ ensure
+ Thread.current[:available_queries_for_explain] = nil
+ end
+
+ def test_collecting_queries_for_explain
+ result, queries = ActiveRecord::Base.collecting_queries_for_explain do
+ Car.where(:name => 'honda').all
+ end
+
+ sql, binds = queries[0]
+ assert_match "SELECT", sql
+ assert_match "honda", sql
+ assert_equal [], binds
+ assert_equal [cars(:honda)], result
+ end
+
+ def test_exec_explain_with_no_binds
+ sqls = %w(foo bar)
+ binds = [[], []]
+ queries = sqls.zip(binds)
+
+ connection.stubs(:explain).returns('query plan foo', 'query plan bar')
+ expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
+ assert_equal expected, base.exec_explain(queries)
+ end
+
+ def test_exec_explain_with_binds
+ cols = [Object.new, Object.new]
+ cols[0].expects(:name).returns('wadus')
+ cols[1].expects(:name).returns('chaflan')
+
+ sqls = %w(foo bar)
+ binds = [[[cols[0], 1]], [[cols[1], 2]]]
+ queries = sqls.zip(binds)
+
+ connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n")
+ expected = <<-SQL.strip_heredoc
+ EXPLAIN for: #{sqls[0]} [["wadus", 1]]
+ query plan foo
+
+ EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
+ query plan bar
+ SQL
+ assert_equal expected, base.exec_explain(queries)
+ end
+
+ def test_silence_auto_explain
+ base.expects(:collecting_sqls_for_explain).never
+ base.logger.expects(:warn).never
+ base.silence_auto_explain do
+ with_threshold(0) { Car.all }
+ end
+ end
+
+ def with_threshold(threshold)
+ current_threshold = base.auto_explain_threshold_in_seconds
+ base.auto_explain_threshold_in_seconds = threshold
+ yield
+ ensure
+ base.auto_explain_threshold_in_seconds = current_threshold
+ end
+ end
+end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 05c4b15407..7d80a56858 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -1119,10 +1119,10 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
- assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => ' author_addresses.id DESC ', :limit => 2).size
+ assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).size
assert_equal 3, Post.find(:all, :include => { :author => :author_address, :authors => :author_address},
- :order => ' author_addresses_authors.id DESC ', :limit => 3).size
+ :order => 'author_addresses_authors.id DESC ', :limit => 3).size
end
def test_find_with_nil_inside_set_passed_for_one_attribute
@@ -1149,7 +1149,10 @@ class FinderTest < ActiveRecord::TestCase
end
def test_with_limiting_with_custom_select
- posts = Post.find(:all, :include => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id')
+ posts = Post.find(
+ :all, :include => :author, :select => ' posts.*, authors.id as "author_id"',
+ :references => :authors, :limit => 3, :order => 'posts.id'
+ )
assert_equal 3, posts.size
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
@@ -1163,7 +1166,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_one_message_with_custom_primary_key
- Toy.set_primary_key :name
+ Toy.primary_key = :name
begin
Toy.find 'Hello World!'
rescue ActiveRecord::RecordNotFound => e
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 7e2dafcd01..5c67cbfcce 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -1,31 +1,34 @@
-require "cases/helper"
-require 'models/post'
+require 'cases/helper'
+require 'models/admin'
+require 'models/admin/account'
+require 'models/admin/randomly_named_c1'
+require 'models/admin/user'
require 'models/binary'
-require 'models/topic'
+require 'models/book'
+require 'models/category'
+require 'models/company'
require 'models/computer'
+require 'models/course'
require 'models/developer'
-require 'models/company'
-require 'models/task'
-require 'models/reply'
require 'models/joke'
-require 'models/course'
-require 'models/category'
+require 'models/matey'
require 'models/parrot'
require 'models/pirate'
-require 'models/treasure'
-require 'models/traffic_light'
-require 'models/matey'
+require 'models/post'
+require 'models/randomly_named_c1'
+require 'models/reply'
require 'models/ship'
-require 'models/book'
-require 'models/admin'
-require 'models/admin/account'
-require 'models/admin/user'
+require 'models/task'
+require 'models/topic'
+require 'models/traffic_light'
+require 'models/treasure'
require 'tempfile'
class FixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
self.use_transactional_fixtures = false
+ # other_topics fixture should not be included here
fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights
FIXTURES = %w( accounts binaries companies customers
@@ -62,8 +65,9 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_create_fixtures
- ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots")
- assert Parrot.find_by_name('Curious George'), 'George is in the database'
+ fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots")
+ assert Parrot.find_by_name('Curious George'), 'George is not in the database'
+ assert fixtures.detect { |f| f.name == 'parrots' }, "no fixtures named 'parrots' in #{fixtures.map(&:name).inspect}"
end
def test_multiple_clean_fixtures
@@ -73,6 +77,13 @@ class FixturesTest < ActiveRecord::TestCase
fixtures_array.each { |fixtures| assert_kind_of(ActiveRecord::Fixtures, fixtures) }
end
+ def test_create_symbol_fixtures
+ fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, :collections, :collections => Course) { Course.connection }
+
+ assert Course.find_by_name('Collection'), 'course is not in the database'
+ assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
+ end
+
def test_attributes
topics = create_fixtures("topics").first
assert_equal("The First Topic", topics["first"]["title"])
@@ -93,7 +104,7 @@ class FixturesTest < ActiveRecord::TestCase
# Reset cache to make finds on the new table work
ActiveRecord::Fixtures.reset_cache
- ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t|
+ ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t|
t.column :title, :string
t.column :author_name, :string
t.column :author_email_address, :string
@@ -115,23 +126,36 @@ class FixturesTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_prefix = 'prefix_'
ActiveRecord::Base.table_name_suffix = '_suffix'
- topics = create_fixtures("topics")
-
- first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'David'")
- assert_equal("The First Topic", first_row["title"])
+ other_topic_klass = Class.new(ActiveRecord::Base) do
+ def self.name
+ "OtherTopic"
+ end
+ end
- second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'Mary'")
- assert_nil(second_row["author_email_address"])
+ topics = [create_fixtures("other_topics")].flatten.first
# This checks for a caching problem which causes a bug in the fixtures
# class-level configuration helper.
assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create"
+
+ first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'")
+ assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found"
+ assert_equal("The First Topic", first_row["title"])
+
+ second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'")
+ assert_nil(second_row["author_email_address"])
+
+ assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym
+ # This assertion should preferably be the last in the list, because calling
+ # other_topic_klass.table_name sets a class-level instance variable
+ assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym
+
ensure
# Restore prefix/suffix to its previous values
ActiveRecord::Base.table_name_prefix = old_prefix
ActiveRecord::Base.table_name_suffix = old_suffix
- ActiveRecord::Base.connection.drop_table :prefix_topics_suffix rescue nil
+ ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil
end
end
@@ -179,7 +203,7 @@ class FixturesTest < ActiveRecord::TestCase
#sanity check to make sure that this file never exists
assert Dir[nonexistent_fixture_path+"*"].empty?
- assert_raise(FixturesFileNotFound) do
+ assert_raise(Errno::ENOENT) do
ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path)
end
end
@@ -213,7 +237,7 @@ class FixturesTest < ActiveRecord::TestCase
def test_binary_in_fixtures
data = File.open(ASSETS_ROOT + "/flowers.jpg", 'rb') { |f| f.read }
- data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
+ data.force_encoding('ASCII-8BIT')
data.freeze
assert_equal data, @flowers.data
end
@@ -573,7 +597,7 @@ class FasterFixturesTest < ActiveRecord::TestCase
load_extra_fixture('posts')
assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts')
- self.class.setup_fixture_accessors('posts')
+ self.class.setup_fixture_accessors :posts
assert_equal 'Welcome to the weblog', posts(:welcome).title
end
end
@@ -731,3 +755,34 @@ class FixtureLoadingTest < ActiveRecord::TestCase
ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
end
end
+
+class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
+ ActiveRecord::Fixtures.reset_cache
+
+ set_fixture_class :randomly_named_a9 =>
+ ClassNameThatDoesNotFollowCONVENTIONS,
+ :'admin/randomly_named_a9' =>
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ 'admin/randomly_named_b0' =>
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS
+
+ fixtures :randomly_named_a9, 'admin/randomly_named_a9',
+ :'admin/randomly_named_b0'
+
+ def test_named_accessor_for_randomly_named_fixture_and_class
+ assert_kind_of ClassNameThatDoesNotFollowCONVENTIONS,
+ randomly_named_a9(:first_instance)
+ end
+
+ def test_named_accessor_for_randomly_named_namespaced_fixture_and_class
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ admin_randomly_named_a9(:first_instance)
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ admin_randomly_named_b0(:second_instance)
+ end
+
+ def test_table_name_is_defined_in_the_model
+ assert_equal :randomly_named_table, ActiveRecord::Fixtures::all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal :randomly_named_table, Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name
+ end
+end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 6735bc521b..d16cccdaea 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -2,12 +2,14 @@ require File.expand_path('../../../../load_paths', __FILE__)
require 'config'
-require 'test/unit'
+require 'minitest/autorun'
require 'stringio'
require 'mocha'
require 'active_record'
+require 'active_record/test_case'
require 'active_support/dependencies'
+require 'active_support/logger'
require 'support/config'
require 'support/connection'
@@ -68,14 +70,19 @@ module ActiveRecord
cattr_accessor :log
self.log = []
+ attr_reader :ignore
+
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
+ @ignore = ignore
+ end
+
def call(name, start, finish, message_id, values)
sql = values[:sql]
# FIXME: this seems bad. we should probably have a better way to indicate
# the query was cached
- unless 'CACHE' == values[:name]
- self.class.log << sql unless self.class.ignored_sql.any? { |r| sql =~ r }
- end
+ return if 'CACHE' == values[:name] || ignore =~ sql
+ self.class.log << sql
end
end
@@ -85,7 +92,10 @@ end
unless ENV['FIXTURE_DEBUG']
module ActiveRecord::TestFixtures::ClassMethods
def try_to_load_dependency_with_silence(*args)
- ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)}
+ old = ActiveRecord::Base.logger.level
+ ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR
+ try_to_load_dependency_without_silence(*args)
+ ActiveRecord::Base.logger.level = old
end
alias_method_chain :try_to_load_dependency, :silence
diff --git a/activerecord/test/cases/identity_map/middleware_test.rb b/activerecord/test/cases/identity_map/middleware_test.rb
index 60dcad4586..5b32a1c6d2 100644
--- a/activerecord/test/cases/identity_map/middleware_test.rb
+++ b/activerecord/test/cases/identity_map/middleware_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "rack"
module ActiveRecord
module IdentityMap
@@ -19,6 +20,7 @@ module ActiveRecord
called = false
mw = Middleware.new lambda { |env|
called = true
+ [200, {}, nil]
}
mw.call({})
assert called, 'middleware delegated'
@@ -27,6 +29,7 @@ module ActiveRecord
def test_im_enabled_during_delegation
mw = Middleware.new lambda { |env|
assert IdentityMap.enabled?, 'identity map should be enabled'
+ [200, {}, nil]
}
mw.call({})
end
diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb
new file mode 100644
index 0000000000..8726ba5e51
--- /dev/null
+++ b/activerecord/test/cases/inclusion_test.rb
@@ -0,0 +1,113 @@
+require 'cases/helper'
+require 'models/teapot'
+
+class BasicInclusionModelTest < ActiveRecord::TestCase
+ def test_basic_model
+ Teapot.create!(:name => "Ronnie Kemper")
+ assert_equal "Ronnie Kemper", Teapot.find(1).name
+ end
+
+ def test_initialization
+ t = Teapot.new(:name => "Bob")
+ assert_equal "Bob", t.name
+ end
+
+ def test_inherited_model
+ teapot = CoolTeapot.create!(:name => "Bob")
+ teapot.reload
+
+ assert_equal "Bob", teapot.name
+ assert_equal "mmm", teapot.aaahhh
+ end
+
+ def test_generated_feature_methods
+ assert Teapot < Teapot::GeneratedFeatureMethods
+ end
+
+ def test_exists
+ t = Teapot.create!(:name => "Ronnie Kemper")
+ assert Teapot.exists?(t)
+ end
+
+ def test_predicate_builder
+ t = Teapot.create!(:name => "Bob")
+ assert_equal "Bob", Teapot.where(:id => [t]).first.name
+ assert_equal "Bob", Teapot.where(:id => t).first.name
+ end
+
+ def test_nested_model
+ assert_equal "ceiling_teapots", Ceiling::Teapot.table_name
+ end
+end
+
+class InclusionUnitTest < ActiveRecord::TestCase
+ def setup
+ @klass = Class.new { include ActiveRecord::Model }
+ end
+
+ def test_non_abstract_class
+ assert !@klass.abstract_class?
+ end
+
+ def test_abstract_class
+ @klass.abstract_class = true
+ assert @klass.abstract_class?
+ end
+
+ def test_establish_connection
+ assert @klass.respond_to?(:establish_connection)
+ assert ActiveRecord::Model.respond_to?(:establish_connection)
+ end
+
+ def test_adapter_connection
+ name = "#{ActiveRecord::Base.connection_config[:adapter]}_connection"
+ assert @klass.respond_to?(name)
+ assert ActiveRecord::Model.respond_to?(name)
+ end
+
+ def test_connection_handler
+ assert_equal ActiveRecord::Base.connection_handler, @klass.connection_handler
+ end
+
+ def test_mirrored_configuration
+ ActiveRecord::Base.time_zone_aware_attributes = true
+ assert @klass.time_zone_aware_attributes
+ ActiveRecord::Base.time_zone_aware_attributes = false
+ assert !@klass.time_zone_aware_attributes
+ ensure
+ ActiveRecord::Base.time_zone_aware_attributes = false
+ end
+
+ # Doesn't really test anything, but this is here to ensure warnings don't occur
+ def test_included_twice
+ @klass.send :include, ActiveRecord::Model
+ end
+
+ def test_deprecation_proxy
+ assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name
+ assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass }
+
+ sup, sup2 = nil, nil
+ ActiveSupport.on_load(:__test_active_record_model_deprecation) do
+ sup = superclass
+ sup2 = send(:superclass)
+ end
+ assert_deprecated do
+ ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy)
+ end
+ assert_equal ActiveRecord::Base.superclass, sup
+ assert_equal ActiveRecord::Base.superclass, sup2
+ end
+end
+
+class InclusionFixturesTest < ActiveRecord::TestCase
+ fixtures :teapots
+
+ def test_fixtured_record
+ assert_equal "Bob", teapots(:bob).name
+ end
+
+ def test_timestamped_fixture
+ assert_not_nil teapots(:bob).created_at
+ end
+end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index b5d8314541..02df464469 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -65,7 +65,6 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_company_descends_from_active_record
- assert_raise(NoMethodError) { ActiveRecord::Base.descends_from_active_record? }
assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
@@ -236,11 +235,11 @@ class InheritanceTest < ActiveRecord::TestCase
c.save
end
[ Company, Firm, Client].each { |klass| klass.reset_column_information }
- Company.set_inheritance_column('ruby_type')
+ Company.inheritance_column = 'ruby_type'
end
def switch_to_default_inheritance_column
[ Company, Firm, Client].each { |klass| klass.reset_column_information }
- Company.set_inheritance_column('type')
+ Company.inheritance_column = 'type'
end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 3ae7b63dff..8f1cdd47ea 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -88,5 +88,17 @@ module ActiveRecord
LegacyMigration.down
assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
+
+ def test_migrate_down_with_table_name_prefix
+ ActiveRecord::Base.table_name_prefix = 'p_'
+ ActiveRecord::Base.table_name_suffix = '_s'
+ migration = InvertibleMigration.new
+ migration.migrate(:up)
+ assert_nothing_raised { migration.migrate(:down) }
+ assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
+ ensure
+ ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ''
+ end
+
end
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index e9bd7f07b6..807274ca67 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -6,12 +6,15 @@ require 'models/reader'
require 'models/legacy_thing'
require 'models/reference'
require 'models/string_key_object'
+require 'models/car'
+require 'models/engine'
+require 'models/wheel'
class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
- set_table_name :lock_without_defaults_cust
- set_locking_column :custom_lock_version
+ self.table_name = :lock_without_defaults_cust
+ self.locking_column = :custom_lock_version
end
class ReadonlyFirstNamePerson < Person
@@ -224,6 +227,18 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal lock_version, p1.lock_version
end
end
+
+ def test_polymorphic_destroy_with_dependencies_and_lock_version
+ car = Car.create!
+
+ assert_difference 'car.wheels.count' do
+ car.wheels << Wheel.create!
+ end
+ assert_difference 'car.wheels.count', -1 do
+ car.destroy
+ end
+ assert car.destroyed?
+ end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
@@ -278,6 +293,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
assert_equal true, p1.frozen?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
+ ensure
+ remove_counter_column_from(Person, 'legacy_things_count')
end
private
@@ -289,8 +306,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
- def remove_counter_column_from(model)
- model.connection.remove_column model.table_name, :test_count
+ def remove_counter_column_from(model, col = :test_count)
+ model.connection.remove_column model.table_name, col
model.reset_column_information
end
@@ -371,22 +388,32 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
end
end
+ def test_with_lock_commits_transaction
+ person = Person.find 1
+ person.with_lock do
+ person.first_name = 'fooman'
+ person.save!
+ end
+ assert_equal 'fooman', person.reload.first_name
+ end
+
+ def test_with_lock_rolls_back_transaction
+ person = Person.find 1
+ old = person.first_name
+ person.with_lock do
+ person.first_name = 'fooman'
+ person.save!
+ raise 'oops'
+ end rescue nil
+ assert_equal old, person.reload.first_name
+ end
+
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_no_locks_no_wait
first, second = duel { Person.find 1 }
assert first.end > second.end
end
- # Hit by ruby deadlock detection since connection checkout is mutexed.
- if RUBY_VERSION < '1.9.0'
- def test_second_lock_waits
- assert [0.2, 1, 5].any? { |zzz|
- first, second = duel(zzz) { Person.find 1, :lock => true }
- second.end > first.end
- }
- end
- end
-
protected
def duel(zzz = 5)
t0, t1, t2, t3 = nil, nil, nil, nil
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 9e8475465e..d1f0ace184 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -5,7 +5,7 @@ require "active_support/log_subscriber/test_helper"
class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
- include ActiveSupport::BufferedLogger::Severity
+ include ActiveSupport::Logger::Severity
fixtures :posts
@@ -13,6 +13,7 @@ class LogSubscriberTest < ActiveRecord::TestCase
@old_logger = ActiveRecord::Base.logger
@using_identity_map = ActiveRecord::IdentityMap.enabled?
ActiveRecord::IdentityMap.enabled = false
+ Developer.primary_key
super
ActiveRecord::LogSubscriber.attach_to(:active_record)
end
diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb
index 9fff50edcb..8122857f52 100644
--- a/activerecord/test/cases/mass_assignment_security_test.rb
+++ b/activerecord/test/cases/mass_assignment_security_test.rb
@@ -50,6 +50,13 @@ module MassAssignmentTestHelpers
assert_equal 'm', person.gender
assert_equal 'rides a sweet bike', person.comments
end
+
+ def with_strict_sanitizer
+ ActiveRecord::Base.mass_assignment_sanitizer = :strict
+ yield
+ ensure
+ ActiveRecord::Base.mass_assignment_sanitizer = :logger
+ end
end
module MassAssignmentRelationTestHelpers
@@ -323,6 +330,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_one_build_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.build_best_friend(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
# create
def test_has_one_create_with_attr_protected_attributes
@@ -350,6 +364,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_one_create_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.create_best_friend(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
# create!
def test_has_one_create_with_bang_with_attr_protected_attributes
@@ -377,6 +398,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_one_create_with_bang_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.create_best_friend!(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
end
@@ -438,6 +466,13 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_belongs_to_create_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.create_best_friend_of(attributes_hash.except(:id, :comments))
+ assert_equal best_friend.id, @person.best_friend_of_id
+ end
+ end
+
# create!
def test_belongs_to_create_with_bang_with_attr_protected_attributes
@@ -465,6 +500,13 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_belongs_to_create_with_bang_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.create_best_friend_of!(attributes_hash.except(:id, :comments))
+ assert_equal best_friend.id, @person.best_friend_of_id
+ end
+ end
+
end
@@ -499,6 +541,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_many_build_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.best_friends.build(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
# create
def test_has_many_create_with_attr_protected_attributes
@@ -526,6 +575,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_many_create_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.best_friends.create(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
# create!
def test_has_many_create_with_bang_with_attr_protected_attributes
@@ -553,6 +609,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase
assert_all_attributes(best_friend)
end
+ def test_has_many_create_with_bang_with_strict_sanitizer
+ with_strict_sanitizer do
+ best_friend = @person.best_friends.create!(attributes_hash.except(:id, :comments))
+ assert_equal @person.id, best_friend.best_friend_id
+ end
+ end
+
end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index 0ab4f30363..ebf6e26385 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -104,7 +104,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_find_include
# with the include, will retrieve only developers for the given project
scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do
- Developer.find(:all, :conditions => 'projects.id = 2')
+ Developer.find(:all, :conditions => { 'projects.id' => 2 })
end
assert scoped_developers.include?(developers(:david))
assert !scoped_developers.include?(developers(:jamis))
@@ -204,7 +204,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_count_include
# with the include, will retrieve only developers for the given project
Developer.send(:with_scope, :find => { :include => :projects }) do
- assert_equal 1, Developer.count(:conditions => 'projects.id = 2')
+ assert_equal 1, Developer.count(:conditions => { 'projects.id' => 2 })
end
end
@@ -339,7 +339,7 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_nested_scoped_find_include
Developer.send(:with_scope, :find => { :include => :projects }) do
- Developer.send(:with_scope, :find => { :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :conditions => { 'projects.id' => 2 } }) do
assert_nothing_raised { Developer.find(1) }
assert_equal('David', Developer.find(:first).name)
end
@@ -348,7 +348,7 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_nested_scoped_find_merged_include
# :include's remain unique and don't "double up" when merging
- Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do
Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal 'David', Developer.find(:first).name
@@ -356,7 +356,7 @@ class NestedScopingTest < ActiveRecord::TestCase
end
# the nested scope doesn't remove the first :include
- Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do
Developer.send(:with_scope, :find => { :include => [] }) do
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal('David', Developer.find(:first).name)
@@ -364,7 +364,7 @@ class NestedScopingTest < ActiveRecord::TestCase
end
# mixing array and symbol include's will merge correctly
- Developer.send(:with_scope, :find => { :include => [:projects], :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => [:projects], :conditions => { 'projects.id' => 2 } }) do
Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal('David', Developer.find(:first).name)
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
new file mode 100644
index 0000000000..a1ade59e52
--- /dev/null
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -0,0 +1,329 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class ChangeSchemaTest < ActiveRecord::TestCase
+ attr_reader :connection, :table_name
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @table_name = :testings
+ end
+
+ def teardown
+ super
+ connection.drop_table :testings rescue nil
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ end
+
+ def test_create_table_without_id
+ testing_table_with_only_foo_attribute do
+ assert_equal connection.columns(:testings).size, 1
+ end
+ end
+
+ def test_add_column_with_primary_key_attribute
+ testing_table_with_only_foo_attribute do
+ connection.add_column :testings, :id, :primary_key
+ assert_equal connection.columns(:testings).size, 2
+ end
+ end
+
+ def test_create_table_adds_id
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ assert_equal %w(foo id), connection.columns(:testings).map(&:name).sort
+ end
+
+ def test_create_table_with_not_null_column
+ connection.create_table :testings do |t|
+ t.column :foo, :string, :null => false
+ end
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ connection.execute "insert into testings (foo) values (NULL)"
+ end
+ end
+
+ def test_create_table_with_defaults
+ # MySQL doesn't allow defaults on TEXT or BLOB columns.
+ mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+
+ connection.create_table :testings do |t|
+ t.column :one, :string, :default => "hello"
+ t.column :two, :boolean, :default => true
+ t.column :three, :boolean, :default => false
+ t.column :four, :integer, :default => 1
+ t.column :five, :text, :default => "hello" unless mysql
+ end
+
+ columns = connection.columns(:testings)
+ one = columns.detect { |c| c.name == "one" }
+ two = columns.detect { |c| c.name == "two" }
+ three = columns.detect { |c| c.name == "three" }
+ four = columns.detect { |c| c.name == "four" }
+ five = columns.detect { |c| c.name == "five" } unless mysql
+
+ assert_equal "hello", one.default
+ assert_equal true, two.default
+ assert_equal false, three.default
+ assert_equal 1, four.default
+ assert_equal "hello", five.default unless mysql
+ end
+
+ def test_create_table_with_limits
+ connection.create_table :testings do |t|
+ t.column :foo, :string, :limit => 255
+
+ t.column :default_int, :integer
+
+ t.column :one_int, :integer, :limit => 1
+ t.column :four_int, :integer, :limit => 4
+ t.column :eight_int, :integer, :limit => 8
+ t.column :eleven_int, :integer, :limit => 11
+ end
+
+ columns = connection.columns(:testings)
+ foo = columns.detect { |c| c.name == "foo" }
+ assert_equal 255, foo.limit
+
+ default = columns.detect { |c| c.name == "default_int" }
+ one = columns.detect { |c| c.name == "one_int" }
+ four = columns.detect { |c| c.name == "four_int" }
+ eight = columns.detect { |c| c.name == "eight_int" }
+ eleven = columns.detect { |c| c.name == "eleven_int" }
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'integer', default.sql_type
+ assert_equal 'smallint', one.sql_type
+ assert_equal 'integer', four.sql_type
+ assert_equal 'bigint', eight.sql_type
+ assert_equal 'integer', eleven.sql_type
+ elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ assert_match 'int(11)', default.sql_type
+ assert_match 'tinyint', one.sql_type
+ assert_match 'int', four.sql_type
+ assert_match 'bigint', eight.sql_type
+ assert_match 'int(11)', eleven.sql_type
+ elsif current_adapter?(:OracleAdapter)
+ assert_equal 'NUMBER(38)', default.sql_type
+ assert_equal 'NUMBER(1)', one.sql_type
+ assert_equal 'NUMBER(4)', four.sql_type
+ assert_equal 'NUMBER(8)', eight.sql_type
+ end
+ end
+
+ def test_create_table_with_primary_key_prefix_as_table_name_with_underscore
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ assert_equal %w(foo testing_id), connection.columns(:testings).map(&:name).sort
+ end
+
+ def test_create_table_with_primary_key_prefix_as_table_name
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ assert_equal %w(foo testingid), connection.columns(:testings).map(&:name).sort
+ end
+
+ def test_create_table_with_timestamps_should_create_datetime_columns
+ connection.create_table table_name do |t|
+ t.timestamps
+ end
+ created_columns = connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert !created_at_column.null
+ assert !updated_at_column.null
+ end
+
+ def test_create_table_with_timestamps_should_create_datetime_columns_with_options
+ connection.create_table table_name do |t|
+ t.timestamps :null => false
+ end
+ created_columns = connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert !created_at_column.null
+ assert !updated_at_column.null
+ end
+
+ def test_create_table_without_a_block
+ connection.create_table table_name
+ end
+
+ def test_add_column_not_null_without_default
+ # Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # column to a table without a default value.
+ if current_adapter?(:SybaseAdapter, :SQLite3Adapter)
+ skip "not supported on #{connection.class}"
+ end
+
+
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+ connection.add_column :testings, :bar, :string, :null => false
+
+ assert_raise(ActiveRecord::StatementInvalid) do
+ connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ end
+ end
+
+ def test_add_column_not_null_with_default
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ con = connection
+ connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
+ connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
+ connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
+ assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ unless current_adapter?(:OpenBaseAdapter)
+ connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ else
+ connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
+ "Testing Insert","id",2)
+ end
+ end
+ end
+
+ def test_change_column_quotes_column_names
+ connection.create_table :testings do |t|
+ t.column :select, :string
+ end
+
+ connection.change_column :testings, :select, :string, :limit => 10
+
+ # Oracle needs primary key value from sequence
+ if current_adapter?(:OracleAdapter)
+ connection.execute "insert into testings (id, #{connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')"
+ else
+ connection.execute "insert into testings (#{connection.quote_column_name('select')}) values ('7 chars')"
+ end
+ end
+
+ def test_keeping_default_and_notnull_constaint_on_change
+ connection.create_table :testings do |t|
+ t.column :title, :string
+ end
+ person_klass = Class.new(ActiveRecord::Base)
+ person_klass.table_name = 'testings'
+
+ person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
+ person_klass.reset_column_information
+ assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal false, person_klass.columns_hash["wealth"].null
+ # Oracle needs primary key value from sequence
+ if current_adapter?(:OracleAdapter)
+ assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")}
+ else
+ assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")}
+ end
+
+ # change column default to see that column doesn't lose its not null definition
+ person_klass.connection.change_column_default "testings", "wealth", 100
+ person_klass.reset_column_information
+ assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal false, person_klass.columns_hash["wealth"].null
+
+ # rename column to see that column doesn't lose its not null and/or default definition
+ person_klass.connection.rename_column "testings", "wealth", "money"
+ person_klass.reset_column_information
+ assert_nil person_klass.columns_hash["wealth"]
+ assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal false, person_klass.columns_hash["money"].null
+
+ # change column
+ person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
+ person_klass.reset_column_information
+ assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal false, person_klass.columns_hash["money"].null
+
+ # change column, make it nullable and clear default
+ person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil
+ person_klass.reset_column_information
+ assert_nil person_klass.columns_hash["money"].default
+ assert_equal true, person_klass.columns_hash["money"].null
+
+ # change_column_null, make it not nullable and set null values to a default value
+ person_klass.connection.execute('UPDATE testings SET money = NULL')
+ person_klass.connection.change_column_null "testings", "money", false, 2000
+ person_klass.reset_column_information
+ assert_nil person_klass.columns_hash["money"].default
+ assert_equal false, person_klass.columns_hash["money"].null
+ assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i
+ end
+
+ def test_column_exists
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+
+ assert connection.column_exists?(:testings, :foo)
+ refute connection.column_exists?(:testings, :bar)
+ end
+
+ def test_column_exists_with_type
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ t.column :bar, :decimal, :precision => 8, :scale => 2
+ end
+
+ assert connection.column_exists?(:testings, :foo, :string)
+ refute connection.column_exists?(:testings, :foo, :integer)
+
+ assert connection.column_exists?(:testings, :bar, :decimal)
+ refute connection.column_exists?(:testings, :bar, :integer)
+ end
+
+ def test_column_exists_with_definition
+ connection.create_table :testings do |t|
+ t.column :foo, :string, :limit => 100
+ t.column :bar, :decimal, :precision => 8, :scale => 2
+ end
+
+ assert connection.column_exists?(:testings, :foo, :string, :limit => 100)
+ refute connection.column_exists?(:testings, :foo, :string, :limit => 50)
+ assert connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2)
+ refute connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2)
+ end
+
+ def test_column_exists_on_table_with_no_options_parameter_supplied
+ connection.create_table :testings do |t|
+ t.string :foo
+ end
+ connection.change_table :testings do |t|
+ assert t.column_exists?(:foo)
+ assert !(t.column_exists?(:bar))
+ end
+ end
+
+ private
+ def testing_table_with_only_foo_attribute
+ connection.create_table :testings, :id => false do |t|
+ t.column :foo, :string
+ end
+
+ yield
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
new file mode 100644
index 0000000000..040445ef12
--- /dev/null
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -0,0 +1,188 @@
+require "cases/migration/helper"
+
+module ActiveRecord
+ class Migration
+ class ColumnAttributesTest < ActiveRecord::TestCase
+ include ActiveRecord::Migration::TestHelper
+
+ self.use_transactional_fixtures = false
+
+ def test_add_remove_single_field_using_string_arguments
+ refute TestModel.column_methods_hash.key?(:last_name)
+
+ add_column 'test_models', 'last_name', :string
+
+ TestModel.reset_column_information
+
+ assert TestModel.column_methods_hash.key?(:last_name)
+
+ remove_column 'test_models', 'last_name'
+
+ TestModel.reset_column_information
+ refute TestModel.column_methods_hash.key?(:last_name)
+ end
+
+ def test_add_remove_single_field_using_symbol_arguments
+ refute TestModel.column_methods_hash.key?(:last_name)
+
+ add_column :test_models, :last_name, :string
+
+ TestModel.reset_column_information
+ assert TestModel.column_methods_hash.key?(:last_name)
+
+ remove_column :test_models, :last_name
+
+ TestModel.reset_column_information
+ refute TestModel.column_methods_hash.key?(:last_name)
+ end
+
+ def test_unabstracted_database_dependent_types
+ skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+
+ add_column :test_models, :intelligence_quotient, :tinyint
+ TestModel.reset_column_information
+ assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ end
+
+ # We specifically do a manual INSERT here, and then test only the SELECT
+ # functionality. This allows us to more easily catch INSERT being broken,
+ # but SELECT actually working fine.
+ def test_native_decimal_insert_manual_vs_automatic
+ correct_value = '0012345678901234567890.0123456789'.to_d
+
+ connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+
+ # Do a manual insertion
+ if current_adapter?(:OracleAdapter)
+ connection.execute "insert into test_models (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)"
+ elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
+ connection.execute "insert into test_models (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)"
+ elsif current_adapter?(:PostgreSQLAdapter)
+ connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, now(), now())"
+ else
+ connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, 0, 0)"
+ end
+
+ # SELECT
+ row = TestModel.find(:first)
+ assert_kind_of BigDecimal, row.wealth
+
+ # If this assert fails, that means the SELECT is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
+
+ # Reset to old state
+ TestModel.delete_all
+
+ # Now use the Rails insertion
+ TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
+
+ # SELECT
+ row = TestModel.find(:first)
+ assert_kind_of BigDecimal, row.wealth
+
+ # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
+ end
+
+ def test_add_column_with_precision_and_scale
+ connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
+
+ def test_change_column_preserve_other_column_precision_and_scale
+ skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter)
+
+ connection.add_column 'test_models', 'last_name', :string
+ connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+
+ connection.change_column 'test_models', 'last_name', :string, :null => false
+ TestModel.reset_column_information
+
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
+
+ def test_native_types
+ add_column "test_models", "first_name", :string
+ add_column "test_models", "last_name", :string
+ add_column "test_models", "bio", :text
+ add_column "test_models", "age", :integer
+ add_column "test_models", "height", :float
+ add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ add_column "test_models", "birthday", :datetime
+ add_column "test_models", "favorite_day", :date
+ add_column "test_models", "moment_of_truth", :datetime
+ add_column "test_models", "male", :boolean
+
+ TestModel.create :first_name => 'bob', :last_name => 'bobsen',
+ :bio => "I was born ....", :age => 18, :height => 1.78,
+ :wealth => BigDecimal.new("12345678901234567890.0123456789"),
+ :birthday => 18.years.ago, :favorite_day => 10.days.ago,
+ :moment_of_truth => "1782-10-10 21:40:18", :male => true
+
+ bob = TestModel.find(:first)
+ assert_equal 'bob', bob.first_name
+ assert_equal 'bobsen', bob.last_name
+ assert_equal "I was born ....", bob.bio
+ assert_equal 18, bob.age
+
+ # Test for 30 significant digits (beyond the 16 of float), 10 of them
+ # after the decimal place.
+
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
+ end
+
+ assert_equal true, bob.male?
+
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
+
+ if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Sybase, and Oracle don't differentiate between date/time
+ assert_equal Time, bob.favorite_day.class
+ else
+ assert_equal Date, bob.favorite_day.class
+ end
+
+ # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column
+ # therefore no timezone change is done afterwards when default timezone is changed
+ unless current_adapter?(:OracleAdapter)
+ # Test DateTime column and defaults, including timezone.
+ # FIXME: moment of truth may be Time on 64-bit platforms.
+ if bob.moment_of_truth.is_a?(DateTime)
+
+ with_env_tz 'US/Eastern' do
+ bob.reload
+ assert_equal DateTime.local_offset, bob.moment_of_truth.offset
+ assert_not_equal 0, bob.moment_of_truth.offset
+ assert_not_equal "Z", bob.moment_of_truth.zone
+ # US/Eastern is -5 hours from GMT
+ assert_equal Rational(-5, 24), bob.moment_of_truth.offset
+ assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM
+ assert_equal DateTime::ITALY, bob.moment_of_truth.start
+ end
+ end
+ end
+
+ assert_instance_of TrueClass, bob.male?
+ assert_kind_of BigDecimal, bob.wealth
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
new file mode 100644
index 0000000000..913d935f7c
--- /dev/null
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -0,0 +1,60 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class ColumnPositioningTest < ActiveRecord::TestCase
+ attr_reader :connection, :table_name
+ alias :conn :connection
+
+ def setup
+ super
+
+ unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ skip "not supported on #{connection.class}"
+ end
+
+ @connection = ActiveRecord::Base.connection
+
+ connection.create_table :testings, :id => false do |t|
+ t.column :first, :integer
+ t.column :second, :integer
+ t.column :third, :integer
+ end
+ end
+
+ def teardown
+ super
+ connection.drop_table :testings rescue nil
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ end
+
+ def test_column_positioning
+ assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
+ end
+
+ def test_add_column_with_positioning
+ conn.add_column :testings, :new_col, :integer
+ assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
+ end
+
+ def test_add_column_with_positioning_first
+ conn.add_column :testings, :new_col, :integer, :first => true
+ assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
+ end
+
+ def test_add_column_with_positioning_after
+ conn.add_column :testings, :new_col, :integer, :after => :first
+ assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
+ end
+
+ def test_change_column_with_positioning
+ conn.change_column :testings, :second, :integer, :first => true
+ assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+
+ conn.change_column :testings, :second, :integer, :after => :third
+ assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ end
+
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index d108b456f0..8f136bce2b 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -67,6 +67,12 @@ module ActiveRecord
assert_equal [:drop_table, [:system_settings]], drop_table
end
+ def test_invert_create_table_with_options
+ @recorder.record :create_table, [:people_reminders, {:id => false}]
+ drop_table = @recorder.inverse.first
+ assert_equal [:drop_table, [:people_reminders]], drop_table
+ end
+
def test_invert_rename_table
@recorder.record :rename_table, [:old, :new]
rename = @recorder.inverse.first
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
new file mode 100644
index 0000000000..fe53510ba2
--- /dev/null
+++ b/activerecord/test/cases/migration/helper.rb
@@ -0,0 +1,64 @@
+require "cases/helper"
+
+module ActiveRecord
+ class Migration
+ class << self
+ attr_accessor :message_count
+ end
+
+ def puts(text="")
+ ActiveRecord::Migration.message_count ||= 0
+ ActiveRecord::Migration.message_count += 1
+ end
+
+ module TestHelper
+ attr_reader :connection, :table_name
+
+ class TestModel < ActiveRecord::Base
+ self.table_name = 'test_models'
+ end
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ connection.create_table :test_models do |t|
+ t.timestamps
+ end
+
+ TestModel.reset_column_information
+ end
+
+ def teardown
+ super
+ TestModel.reset_table_name
+ TestModel.reset_sequence_name
+ connection.drop_table :test_models rescue nil
+ end
+
+ private
+ def add_column(*args)
+ connection.add_column(*args)
+ end
+
+ def remove_column(*args)
+ connection.remove_column(*args)
+ end
+
+ def rename_column(*args)
+ connection.rename_column(*args)
+ end
+
+ def add_index(*args)
+ connection.add_index(*args)
+ end
+
+ def change_column(*args)
+ connection.change_column(*args)
+ end
+
+ def rename_table(*args)
+ connection.rename_table(*args)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
new file mode 100644
index 0000000000..26d7aeb148
--- /dev/null
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -0,0 +1,170 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class IndexTest < ActiveRecord::TestCase
+ attr_reader :connection, :table_name
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @table_name = :testings
+
+ connection.create_table table_name do |t|
+ t.column :foo, :string, :limit => 100
+ t.column :bar, :string, :limit => 100
+
+ t.string :first_name
+ t.string :last_name, :limit => 100
+ t.string :key, :limit => 100
+ t.boolean :administrator
+ end
+ end
+
+ def teardown
+ super
+ connection.drop_table :testings rescue nil
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ end
+
+ def test_rename_index
+ skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ connection.rename_index(table_name, 'old_idx', 'new_idx')
+
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ refute connection.index_name_exists?(table_name, 'old_idx', false)
+ assert connection.index_name_exists?(table_name, 'new_idx', true)
+ end
+
+ def test_double_add_index
+ skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ assert_raises(ArgumentError) {
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ }
+ end
+
+ def test_remove_nonexistent_index
+ skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+
+ # we do this by name, so OpenBase is a wash as noted above
+ assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
+ end
+
+ def test_add_index_length_limit
+ good_index_name = 'x' * connection.index_name_length
+ too_long_index_name = good_index_name + 'x'
+
+ assert_raises(ArgumentError) {
+ connection.add_index(table_name, "foo", :name => too_long_index_name)
+ }
+
+ refute connection.index_name_exists?(table_name, too_long_index_name, false)
+ connection.add_index(table_name, "foo", :name => good_index_name)
+
+ assert connection.index_name_exists?(table_name, good_index_name, false)
+ connection.remove_index(table_name, :name => good_index_name)
+ end
+
+ def test_index_symbol_names
+ connection.add_index table_name, :foo, :name => :symbol_index_name
+ assert connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+
+ connection.remove_index table_name, :name => :symbol_index_name
+ refute connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+ end
+
+ def test_index_exists
+ connection.add_index :testings, :foo
+
+ assert connection.index_exists?(:testings, :foo)
+ assert !connection.index_exists?(:testings, :bar)
+ end
+
+ def test_index_exists_on_multiple_columns
+ connection.add_index :testings, [:foo, :bar]
+
+ assert connection.index_exists?(:testings, [:foo, :bar])
+ end
+
+ def test_unique_index_exists
+ connection.add_index :testings, :foo, :unique => true
+
+ assert connection.index_exists?(:testings, :foo, :unique => true)
+ end
+
+ def test_named_index_exists
+ connection.add_index :testings, :foo, :name => "custom_index_name"
+
+ assert connection.index_exists?(:testings, :foo, :name => "custom_index_name")
+ end
+
+ def test_add_index
+ connection.add_index("testings", "last_name")
+ connection.remove_index("testings", "last_name")
+
+ # Orcl nds shrt indx nms. Sybs 2.
+ # OpenBase does not have named indexes. You must specify a single column name
+ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", :column => ["last_name", "first_name"])
+
+ # Oracle adapter cannot have specified index name larger than 30 characters
+ # Oracle adapter is shortening index name when just column list is given
+ unless current_adapter?(:OracleAdapter)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", "last_name_and_first_name")
+ end
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", ["last_name", "first_name"])
+
+ connection.add_index("testings", ["last_name"], :length => 10)
+ connection.remove_index("testings", "last_name")
+
+ connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
+ connection.remove_index("testings", ["last_name"])
+
+ connection.add_index("testings", ["last_name", "first_name"], :length => 10)
+ connection.remove_index("testings", ["last_name", "first_name"])
+
+ connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
+ connection.remove_index("testings", ["last_name", "first_name"])
+ end
+
+ # quoting
+ # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
+ # OpenBase does not have named indexes. You must specify a single column name
+ unless current_adapter?(:OpenBaseAdapter)
+ connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
+ connection.remove_index("testings", :name => "key_idx", :unique => true)
+ end
+
+ # Sybase adapter does not support indexes on :boolean columns
+ # OpenBase does not have named indexes. You must specify a single column
+ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
+ connection.remove_index("testings", :name => "named_admin")
+ end
+
+ # Selected adapters support index sort order
+ if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ connection.add_index("testings", ["last_name"], :order => {:last_name => :desc})
+ connection.remove_index("testings", ["last_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc})
+ connection.remove_index("testings", ["last_name", "first_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc})
+ connection.remove_index("testings", ["last_name", "first_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :order => :desc)
+ connection.remove_index("testings", ["last_name", "first_name"])
+ end
+ end
+
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
new file mode 100644
index 0000000000..ee0c20747e
--- /dev/null
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -0,0 +1,37 @@
+require "cases/helper"
+
+module ActiveRecord
+ class Migration
+ class LoggerTest < ActiveRecord::TestCase
+ # mysql can't roll back ddl changes
+ self.use_transactional_fixtures = false
+
+ Migration = Struct.new(:name, :version) do
+ def migrate direction
+ # do nothing
+ end
+ end
+
+ def setup
+ super
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::SchemaMigration.delete_all
+ end
+
+ def teardown
+ super
+ ActiveRecord::SchemaMigration.drop_table
+ end
+
+ def test_migration_should_be_run_without_logger
+ previous_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+ migrations = [Migration.new('a', 1), Migration.new('b', 2), Migration.new('c', 3)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ ensure
+ ActiveRecord::Base.logger = previous_logger
+ end
+ end
+ end
+end
+
diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb
new file mode 100644
index 0000000000..2e7e533ed6
--- /dev/null
+++ b/activerecord/test/cases/migration/rename_column_test.rb
@@ -0,0 +1,195 @@
+require "cases/migration/helper"
+
+module ActiveRecord
+ class Migration
+ class RenameColumnTest < ActiveRecord::TestCase
+ include ActiveRecord::Migration::TestHelper
+
+ self.use_transactional_fixtures = false
+
+ # FIXME: this is more of an integration test with AR::Base and the
+ # schema modifications. Maybe we should move this?
+ def test_add_rename
+ add_column "test_models", "girlfriend", :string
+ TestModel.reset_column_information
+
+ TestModel.create :girlfriend => 'bobette'
+
+ rename_column "test_models", "girlfriend", "exgirlfriend"
+
+ TestModel.reset_column_information
+ bob = TestModel.find(:first)
+
+ assert_equal "bobette", bob.exgirlfriend
+ end
+
+ # FIXME: another integration test. We should decouple this from the
+ # AR::Base implementation.
+ def test_rename_column_using_symbol_arguments
+ add_column :test_models, :first_name, :string
+
+ TestModel.create :first_name => 'foo'
+
+ rename_column :test_models, :first_name, :nick_name
+ TestModel.reset_column_information
+ assert TestModel.column_names.include?("nick_name")
+ assert_equal ['foo'], TestModel.find(:all).map(&:nick_name)
+ end
+
+ # FIXME: another integration test. We should decouple this from the
+ # AR::Base implementation.
+ def test_rename_column
+ add_column "test_models", "first_name", "string"
+
+ TestModel.create :first_name => 'foo'
+
+ rename_column "test_models", "first_name", "nick_name"
+ TestModel.reset_column_information
+ assert TestModel.column_names.include?("nick_name")
+ assert_equal ['foo'], TestModel.find(:all).map(&:nick_name)
+ end
+
+ def test_rename_column_preserves_default_value_not_null
+ add_column 'test_models', 'salary', :integer, :default => 70000
+
+ default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
+ assert_equal 70000, default_before
+
+ rename_column "test_models", "salary", "anual_salary"
+
+ assert TestModel.column_names.include?("anual_salary")
+ default_after = connection.columns("test_models").find { |c| c.name == "anual_salary" }.default
+ assert_equal 70000, default_after
+ end
+
+ def test_rename_nonexistent_column
+ exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ ActiveRecord::StatementInvalid
+ else
+ ActiveRecord::ActiveRecordError
+ end
+ assert_raise(exception) do
+ rename_column "test_models", "nonexistent", "should_fail"
+ end
+ end
+
+ def test_rename_column_with_sql_reserved_word
+ add_column 'test_models', 'first_name', :string
+ rename_column "test_models", "first_name", "group"
+
+ assert TestModel.column_names.include?("group")
+ end
+
+ def test_rename_column_with_an_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name
+
+ # FIXME: we should test that the index goes away
+ rename_column "test_models", "hat_name", "name"
+ end
+
+ def test_remove_column_with_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name
+
+ # FIXME: we should test that the index goes away
+ remove_column("test_models", "hat_name")
+ end
+
+ def test_remove_column_with_multi_column_index
+ add_column "test_models", :hat_size, :integer
+ add_column "test_models", :hat_style, :string, :limit => 100
+ add_index "test_models", ["hat_style", "hat_size"], :unique => true
+
+ # FIXME: we should test that the index goes away
+ remove_column("test_models", "hat_size")
+ end
+
+ # FIXME: we need to test that these calls do something
+ def test_change_type_of_not_null_column
+ change_column "test_models", "updated_at", :datetime, :null => false
+ change_column "test_models", "updated_at", :datetime, :null => false
+ change_column "test_models", "updated_at", :datetime, :null => true
+ end
+
+ def test_change_column_nullability
+ add_column "test_models", "funny", :boolean
+ assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
+
+ change_column "test_models", "funny", :boolean, :null => false, :default => true
+
+ TestModel.reset_column_information
+ refute TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
+
+ change_column "test_models", "funny", :boolean, :null => true
+ TestModel.reset_column_information
+ assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
+ end
+
+ def test_change_column
+ add_column 'test_models', 'age', :integer
+ add_column 'test_models', 'approved', :boolean, :default => true
+
+ label = "test_change_column Columns"
+ old_columns = connection.columns(TestModel.table_name, label)
+
+ assert old_columns.find { |c| c.name == 'age' && c.type == :integer }
+
+ change_column "test_models", "age", :string
+
+ new_columns = connection.columns(TestModel.table_name, label)
+
+ refute new_columns.find { |c| c.name == 'age' and c.type == :integer }
+ assert new_columns.find { |c| c.name == 'age' and c.type == :string }
+
+ old_columns = connection.columns(TestModel.table_name, label)
+ assert old_columns.find { |c|
+ c.name == 'approved' && c.type == :boolean && c.default == true
+ }
+
+ change_column :test_models, :approved, :boolean, :default => false
+ new_columns = connection.columns(TestModel.table_name, label)
+
+ refute new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
+ assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
+ change_column :test_models, :approved, :boolean, :default => true
+ end
+
+ def test_change_column_with_nil_default
+ add_column "test_models", "contributor", :boolean, :default => true
+ assert TestModel.new.contributor?
+
+ change_column "test_models", "contributor", :boolean, :default => nil
+ TestModel.reset_column_information
+ refute TestModel.new.contributor?
+ assert_nil TestModel.new.contributor
+ end
+
+ def test_change_column_with_new_default
+ add_column "test_models", "administrator", :boolean, :default => true
+ assert TestModel.new.administrator?
+
+ change_column "test_models", "administrator", :boolean, :default => false
+ TestModel.reset_column_information
+ refute TestModel.new.administrator?
+ end
+
+ def test_change_column_default
+ add_column "test_models", "first_name", :string
+ connection.change_column_default "test_models", "first_name", "Tester"
+
+ assert_equal "Tester", TestModel.new.first_name
+ end
+
+ def test_change_column_default_to_null
+ add_column "test_models", "first_name", :string
+ connection.change_column_default "test_models", "first_name", nil
+ assert_nil TestModel.new.first_name
+ end
+
+ def test_remove_column_no_second_parameter_raises_exception
+ assert_raise(ArgumentError) { connection.remove_column("funny") }
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
new file mode 100644
index 0000000000..d5ff2c607f
--- /dev/null
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -0,0 +1,72 @@
+require "cases/migration/helper"
+
+module ActiveRecord
+ class Migration
+ class RenameTableTest < ActiveRecord::TestCase
+ include ActiveRecord::Migration::TestHelper
+
+ self.use_transactional_fixtures = false
+
+ def setup
+ super
+ add_column 'test_models', :url, :string
+ remove_column 'test_models', :created_at
+ remove_column 'test_models', :updated_at
+ end
+
+ def test_rename_table_for_sqlite_should_work_with_reserved_words
+ renamed = false
+
+ skip "not supported" unless current_adapter?(:SQLite3Adapter)
+
+ add_column :test_models, :url, :string
+ connection.rename_table :references, :old_references
+ connection.rename_table :test_models, :references
+
+ renamed = true
+
+ # Using explicit id in insert for compatibility across all databases
+ con = connection
+ con.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
+ assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
+ ensure
+ return unless renamed
+ connection.rename_table :references, :test_models
+ connection.rename_table :old_references, :references
+ end
+
+ def test_rename_table
+ rename_table :test_models, :octopi
+
+ # Using explicit id in insert for compatibility across all databases
+ con = connection
+ con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
+
+ con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
+
+ con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
+
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+
+ rename_table :octopi, :test_models
+ end
+
+ def test_rename_table_with_an_index
+ add_index :test_models, :url
+
+ rename_table :test_models, :octopi
+
+ # Using explicit id in insert for compatibility across all databases
+ con = ActiveRecord::Base.connection
+ con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
+ con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
+ con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
+
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+ assert connection.indexes(:octopi).first.columns.include?("url")
+
+ rename_table :octopi, :test_models
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb
new file mode 100644
index 0000000000..8fd770abd1
--- /dev/null
+++ b/activerecord/test/cases/migration/table_and_index_test.rb
@@ -0,0 +1,24 @@
+require "cases/helper"
+
+module ActiveRecord
+ class Migration
+ class TableAndIndexTest < ActiveRecord::TestCase
+ def test_add_schema_info_respects_prefix_and_suffix
+ conn = ActiveRecord::Base.connection
+
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
+ ActiveRecord::Base.table_name_prefix = 'p_'
+ ActiveRecord::Base.table_name_suffix = '_s'
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+
+ conn.initialize_schema_migrations_table
+
+ assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name]
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 3e219f2a49..575df2f84b 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "cases/migration/helper"
require 'bigdecimal/util'
require 'models/person'
@@ -6,2209 +7,941 @@ require 'models/topic'
require 'models/developer'
require MIGRATIONS_ROOT + "/valid/2_we_need_reminders"
+require MIGRATIONS_ROOT + "/rename/1_we_need_things"
+require MIGRATIONS_ROOT + "/rename/2_rename_things"
require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
-if ActiveRecord::Base.connection.supports_migrations?
- class BigNumber < ActiveRecord::Base; end
+class BigNumber < ActiveRecord::Base; end
- class Reminder < ActiveRecord::Base; end
+class Reminder < ActiveRecord::Base; end
- class ActiveRecord::Migration
- class << self
- attr_accessor :message_count
- end
-
- def puts(text="")
- ActiveRecord::Migration.message_count ||= 0
- ActiveRecord::Migration.message_count += 1
- end
- end
-
- class MigrationTableAndIndexTest < ActiveRecord::TestCase
- def test_add_schema_info_respects_prefix_and_suffix
- conn = ActiveRecord::Base.connection
+class Thing < ActiveRecord::Base; end
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
- # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
- ActiveRecord::Base.table_name_prefix = 'p_'
- ActiveRecord::Base.table_name_suffix = '_s'
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+class MigrationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- conn.initialize_schema_migrations_table
+ fixtures :people
- assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name]
- ensure
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
+ def setup
+ super
+ %w(reminders people_reminders prefix_reminders_suffix).each do |table|
+ Reminder.connection.drop_table(table) rescue nil
end
+ Reminder.reset_column_information
+ ActiveRecord::Migration.verbose = true
+ ActiveRecord::Migration.message_count = 0
end
- class MigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- fixtures :people
-
- def setup
- ActiveRecord::Migration.verbose = true
- ActiveRecord::Migration.message_count = 0
- end
-
- def teardown
- ActiveRecord::Base.connection.initialize_schema_migrations_table
- ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
-
- %w(reminders people_reminders prefix_reminders_suffix).each do |table|
- Reminder.connection.drop_table(table) rescue nil
- end
- Reminder.reset_column_information
-
- %w(last_name key bio age height wealth birthday favorite_day
- moment_of_truth male administrator funny).each do |column|
- Person.connection.remove_column('people', column) rescue nil
- end
- Person.connection.remove_column("people", "first_name") rescue nil
- Person.connection.remove_column("people", "middle_name") rescue nil
- Person.connection.add_column("people", "first_name", :string, :limit => 40)
- Person.reset_column_information
- end
-
- def test_add_index
- # Limit size of last_name and key columns to support Firebird index limitations
- Person.connection.add_column "people", "last_name", :string, :limit => 100
- Person.connection.add_column "people", "key", :string, :limit => 100
- Person.connection.add_column "people", "administrator", :boolean
-
- assert_nothing_raised { Person.connection.add_index("people", "last_name") }
- assert_nothing_raised { Person.connection.remove_index("people", "last_name") }
-
- # Orcl nds shrt indx nms. Sybs 2.
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.remove_index("people", :column => ["last_name", "first_name"]) }
- # Oracle adapter cannot have specified index name larger than 30 characters
- # Oracle adapter is shortening index name when just column list is given
- unless current_adapter?(:OracleAdapter)
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.remove_index("people", :name => :index_people_on_last_name_and_first_name) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") }
- end
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => 10) }
- assert_nothing_raised { Person.connection.remove_index("people", "last_name") }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => {:last_name => 10}) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => 10) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- end
-
- # quoting
- # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:OpenBaseAdapter)
- Person.update_all "#{Person.connection.quote_column_name 'key'}=#{Person.connection.quote_column_name 'id'}" #some databases (including sqlite2 won't add a unique index if existing data non unique)
- assert_nothing_raised { Person.connection.add_index("people", ["key"], :name => "key_idx", :unique => true) }
- assert_nothing_raised { Person.connection.remove_index("people", :name => "key_idx", :unique => true) }
- end
-
- # Sybase adapter does not support indexes on :boolean columns
- # OpenBase does not have named indexes. You must specify a single column
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
- assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") }
- assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") }
- end
-
- # Selected adapters support index sort order
- if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
- assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :order => {:last_name => :desc}) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc}) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc}) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => :desc) }
- assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
- end
- end
-
- def test_index_symbol_names
- assert_nothing_raised { Person.connection.add_index :people, :primary_contact_id, :name => :symbol_index_name }
- assert Person.connection.index_exists?(:people, :primary_contact_id, :name => :symbol_index_name)
- assert_nothing_raised { Person.connection.remove_index :people, :name => :symbol_index_name }
- assert !Person.connection.index_exists?(:people, :primary_contact_id, :name => :symbol_index_name)
- end
-
- def test_add_index_length_limit
- good_index_name = 'x' * Person.connection.index_name_length
- too_long_index_name = good_index_name + 'x'
- assert_raise(ArgumentError) { Person.connection.add_index("people", "first_name", :name => too_long_index_name) }
- assert !Person.connection.index_name_exists?("people", too_long_index_name, false)
- assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => good_index_name) }
- assert Person.connection.index_name_exists?("people", good_index_name, false)
- Person.connection.remove_index("people", :name => good_index_name)
- end
-
- def test_remove_nonexistent_index
- # we do this by name, so OpenBase is a wash as noted above
- unless current_adapter?(:OpenBaseAdapter)
- assert_raise(ArgumentError) { Person.connection.remove_index("people", "no_such_index") }
- end
- end
-
- def test_rename_index
- unless current_adapter?(:OpenBaseAdapter)
- # keep the names short to make Oracle and similar behave
- Person.connection.add_index('people', [:first_name], :name => 'old_idx')
- assert_nothing_raised { Person.connection.rename_index('people', 'old_idx', 'new_idx') }
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert !Person.connection.index_name_exists?('people', 'old_idx', false)
- assert Person.connection.index_name_exists?('people', 'new_idx', true)
- end
- end
-
- def test_double_add_index
- unless current_adapter?(:OpenBaseAdapter)
- Person.connection.add_index('people', [:first_name], :name => 'some_idx')
- assert_raise(ArgumentError) { Person.connection.add_index('people', [:first_name], :name => 'some_idx') }
- end
- end
-
- def test_index_exists
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- t.column :bar, :string, :limit => 100
- end
- Person.connection.add_index :testings, :foo
-
- assert Person.connection.index_exists?(:testings, :foo)
- assert !Person.connection.index_exists?(:testings, :bar)
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_index_exists_on_multiple_columns
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- t.column :bar, :string, :limit => 100
- end
- Person.connection.add_index :testings, [:foo, :bar]
-
- assert Person.connection.index_exists?(:testings, [:foo, :bar])
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_unique_index_exists
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- end
- Person.connection.add_index :testings, :foo, :unique => true
-
- assert Person.connection.index_exists?(:testings, :foo, :unique => true)
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_named_index_exists
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- end
- Person.connection.add_index :testings, :foo, :name => "custom_index_name"
-
- assert Person.connection.index_exists?(:testings, :foo, :name => "custom_index_name")
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def testing_table_with_only_foo_attribute
- Person.connection.create_table :testings, :id => false do |t|
- t.column :foo, :string
- end
-
- yield Person.connection
- ensure
- Person.connection.drop_table :testings rescue nil
- end
- protected :testing_table_with_only_foo_attribute
+ def teardown
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
+ ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
- def test_create_table_without_id
- testing_table_with_only_foo_attribute do |connection|
- assert_equal connection.columns(:testings).size, 1
- end
+ %w(things awesome_things prefix_things_suffix prefix_awesome_things_suffix).each do |table|
+ Thing.connection.drop_table(table) rescue nil
end
+ Thing.reset_column_information
- def test_add_column_with_primary_key_attribute
- testing_table_with_only_foo_attribute do |connection|
- assert_nothing_raised { connection.add_column :testings, :id, :primary_key }
- assert_equal connection.columns(:testings).size, 2
- end
+ %w(reminders people_reminders prefix_reminders_suffix).each do |table|
+ Reminder.connection.drop_table(table) rescue nil
end
+ Reminder.reset_column_information
- def test_create_table_adds_id
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
- end
-
- assert_equal %w(foo id),
- Person.connection.columns(:testings).map { |c| c.name }.sort
- ensure
- Person.connection.drop_table :testings rescue nil
+ %w(last_name key bio age height wealth birthday favorite_day
+ moment_of_truth male administrator funny).each do |column|
+ Person.connection.remove_column('people', column) rescue nil
end
+ Person.connection.remove_column("people", "first_name") rescue nil
+ Person.connection.remove_column("people", "middle_name") rescue nil
+ Person.connection.add_column("people", "first_name", :string, :limit => 40)
+ Person.reset_column_information
+ end
- def test_create_table_with_not_null_column
- assert_nothing_raised do
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :null => false
- end
- end
-
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute "insert into testings (foo) values (NULL)"
- end
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_create_table_with_defaults
- # MySQL doesn't allow defaults on TEXT or BLOB columns.
- mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
-
- Person.connection.create_table :testings do |t|
- t.column :one, :string, :default => "hello"
- t.column :two, :boolean, :default => true
- t.column :three, :boolean, :default => false
- t.column :four, :integer, :default => 1
- t.column :five, :text, :default => "hello" unless mysql
- end
-
- columns = Person.connection.columns(:testings)
- one = columns.detect { |c| c.name == "one" }
- two = columns.detect { |c| c.name == "two" }
- three = columns.detect { |c| c.name == "three" }
- four = columns.detect { |c| c.name == "four" }
- five = columns.detect { |c| c.name == "five" } unless mysql
-
- assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
- assert_equal "hello", five.default unless mysql
-
- ensure
- Person.connection.drop_table :testings rescue nil
+ def test_create_table_with_force_true_does_not_drop_nonexisting_table
+ if Person.connection.table_exists?(:testings2)
+ Person.connection.drop_table :testings2
end
- def test_create_table_with_limits
- assert_nothing_raised do
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 255
-
- t.column :default_int, :integer
+ # using a copy as we need the drop_table method to
+ # continue to work for the ensure block of the test
+ temp_conn = Person.connection.dup
- t.column :one_int, :integer, :limit => 1
- t.column :four_int, :integer, :limit => 4
- t.column :eight_int, :integer, :limit => 8
- t.column :eleven_int, :integer, :limit => 11
- end
- end
+ assert_not_equal temp_conn, Person.connection
- columns = Person.connection.columns(:testings)
- foo = columns.detect { |c| c.name == "foo" }
- assert_equal 255, foo.limit
-
- default = columns.detect { |c| c.name == "default_int" }
- one = columns.detect { |c| c.name == "one_int" }
- four = columns.detect { |c| c.name == "four_int" }
- eight = columns.detect { |c| c.name == "eight_int" }
- eleven = columns.detect { |c| c.name == "eleven_int" }
-
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'integer', default.sql_type
- assert_equal 'smallint', one.sql_type
- assert_equal 'integer', four.sql_type
- assert_equal 'bigint', eight.sql_type
- assert_equal 'integer', eleven.sql_type
- elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- assert_match 'int(11)', default.sql_type
- assert_match 'tinyint', one.sql_type
- assert_match 'int', four.sql_type
- assert_match 'bigint', eight.sql_type
- assert_match 'int(11)', eleven.sql_type
- elsif current_adapter?(:OracleAdapter)
- assert_equal 'NUMBER(38)', default.sql_type
- assert_equal 'NUMBER(1)', one.sql_type
- assert_equal 'NUMBER(4)', four.sql_type
- assert_equal 'NUMBER(8)', eight.sql_type
- end
- ensure
- Person.connection.drop_table :testings rescue nil
+ temp_conn.create_table :testings2, :force => true do |t|
+ t.column :foo, :string
end
+ ensure
+ Person.connection.drop_table :testings2 rescue nil
+ end
- def test_create_table_with_primary_key_prefix_as_table_name_with_underscore
- ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
-
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
- end
-
- assert_equal %w(foo testing_id), Person.connection.columns(:testings).map { |c| c.name }.sort
- ensure
- Person.connection.drop_table :testings rescue nil
- ActiveRecord::Base.primary_key_prefix_type = nil
- end
+ def connection
+ ActiveRecord::Base.connection
+ end
- def test_create_table_with_primary_key_prefix_as_table_name
- ActiveRecord::Base.primary_key_prefix_type = :table_name
+ def test_migration_instance_has_connection
+ migration = Class.new(ActiveRecord::Migration).new
+ assert_equal connection, migration.connection
+ end
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
+ def test_method_missing_delegates_to_connection
+ migration = Class.new(ActiveRecord::Migration) {
+ def connection
+ Class.new {
+ def create_table; "hi mom!"; end
+ }.new
end
+ }.new
- assert_equal %w(foo testingid), Person.connection.columns(:testings).map { |c| c.name }.sort
- ensure
- Person.connection.drop_table :testings rescue nil
- ActiveRecord::Base.primary_key_prefix_type = nil
- end
-
- def test_create_table_with_force_true_does_not_drop_nonexisting_table
- if Person.connection.table_exists?(:testings2)
- Person.connection.drop_table :testings2
- end
+ assert_equal "hi mom!", migration.method_missing(:create_table)
+ end
- # using a copy as we need the drop_table method to
- # continue to work for the ensure block of the test
- temp_conn = Person.connection.dup
- temp_conn.expects(:drop_table).never
- temp_conn.create_table :testings2, :force => true do |t|
- t.column :foo, :string
- end
- ensure
- Person.connection.drop_table :testings2 rescue nil
- end
+ def test_add_table_with_decimals
+ Person.connection.drop_table :big_numbers rescue nil
+
+ assert !BigNumber.table_exists?
+ GiveMeBigNumbers.up
+
+ assert BigNumber.create(
+ :bank_balance => 1586.43,
+ :big_bank_balance => BigDecimal("1000234000567.95"),
+ :world_population => 6000000000,
+ :my_house_population => 3,
+ :value_of_e => BigDecimal("2.7182818284590452353602875")
+ )
+
+ b = BigNumber.find(:first)
+ assert_not_nil b
+
+ assert_not_nil b.bank_balance
+ assert_not_nil b.big_bank_balance
+ assert_not_nil b.world_population
+ assert_not_nil b.my_house_population
+ assert_not_nil b.value_of_e
+
+ # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
+ # is_a?(Bignum)
+ assert_kind_of Integer, b.world_population
+ assert_equal 6000000000, b.world_population
+ assert_kind_of Fixnum, b.my_house_population
+ assert_equal 3, b.my_house_population
+ assert_kind_of BigDecimal, b.bank_balance
+ assert_equal BigDecimal("1586.43"), b.bank_balance
+ assert_kind_of BigDecimal, b.big_bank_balance
+ assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance
+
+ # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with
+ # precision/scale explicitly left out. By the SQL standard, numbers
+ # assigned to this field should be truncated but that's seldom respected.
+ if current_adapter?(:PostgreSQLAdapter)
+ # - PostgreSQL changes the SQL spec on columns declared simply as
+ # "decimal" to something more useful: instead of being given a scale
+ # of 0, they take on the compile-time limit for precision and scale,
+ # so the following should succeed unless you have used really wacky
+ # compilation options
+ # - SQLite2 has the default behavior of preserving all data sent in,
+ # so this happens there too
+ assert_kind_of BigDecimal, b.value_of_e
+ assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e
+ elsif current_adapter?(:SQLite3Adapter)
+ # - SQLite3 stores a float, in violation of SQL
+ assert_kind_of BigDecimal, b.value_of_e
+ assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001
+ else
+ # - SQL standard is an integer
+ assert_kind_of Fixnum, b.value_of_e
+ assert_equal 2, b.value_of_e
+ end
+
+ GiveMeBigNumbers.down
+ assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
+ end
- def test_create_table_with_timestamps_should_create_datetime_columns
- table_name = :testings
+ def test_filtering_migrations
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert !Reminder.table_exists?
- Person.connection.create_table table_name do |t|
- t.timestamps
- end
- created_columns = Person.connection.columns(table_name)
+ name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
- created_at_column = created_columns.detect {|c| c.name == 'created_at' }
- updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+ Person.reset_column_information
+ assert Person.column_methods_hash.include?(:last_name)
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
- assert !created_at_column.null
- assert !updated_at_column.null
- ensure
- Person.connection.drop_table table_name rescue nil
- end
+ ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
- def test_create_table_with_timestamps_should_create_datetime_columns_with_options
- table_name = :testings
-
- Person.connection.create_table table_name do |t|
- t.timestamps :null => false
- end
- created_columns = Person.connection.columns(table_name)
-
- created_at_column = created_columns.detect {|c| c.name == 'created_at' }
- updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ end
- assert !created_at_column.null
- assert !updated_at_column.null
- ensure
- Person.connection.drop_table table_name rescue nil
+ class MockMigration < ActiveRecord::Migration
+ attr_reader :went_up, :went_down
+ def initialize
+ @went_up = false
+ @went_down = false
end
- def test_create_table_without_a_block
- table_name = :testings
- Person.connection.create_table table_name
- ensure
- Person.connection.drop_table table_name rescue nil
+ def up
+ @went_up = true
+ super
end
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
- # column to a table without a default value.
- unless current_adapter?(:SybaseAdapter, :SQLite3Adapter)
- def test_add_column_not_null_without_default
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
- end
- Person.connection.add_column :testings, :bar, :string, :null => false
-
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
- end
- ensure
- Person.connection.drop_table :testings rescue nil
- end
+ def down
+ @went_down = true
+ super
end
+ end
- def test_add_column_not_null_with_default
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
- end
-
- con = Person.connection
- Person.connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
- Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
- Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
- assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
-
- assert_raise(ActiveRecord::StatementInvalid) do
- unless current_adapter?(:OpenBaseAdapter)
- Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
- else
- Person.connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
- "Testing Insert","id",2)
- end
- end
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- # We specifically do a manual INSERT here, and then test only the SELECT
- # functionality. This allows us to more easily catch INSERT being broken,
- # but SELECT actually working fine.
- def test_native_decimal_insert_manual_vs_automatic
- correct_value = '0012345678901234567890.0123456789'.to_d
-
- Person.delete_all
- Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10'
- Person.reset_column_information
-
- # Do a manual insertion
- if current_adapter?(:OracleAdapter)
- Person.connection.execute "insert into people (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)"
- elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
- Person.connection.execute "insert into people (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)"
- elsif current_adapter?(:PostgreSQLAdapter)
- Person.connection.execute "insert into people (wealth, created_at, updated_at) values (12345678901234567890.0123456789, now(), now())"
- else
- Person.connection.execute "insert into people (wealth, created_at, updated_at) values (12345678901234567890.0123456789, 0, 0)"
- end
-
- # SELECT
- row = Person.find(:first)
- assert_kind_of BigDecimal, row.wealth
-
- # If this assert fails, that means the SELECT is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
-
- # Reset to old state
- Person.delete_all
-
- # Now use the Rails insertion
- assert_nothing_raised { Person.create :wealth => BigDecimal.new("12345678901234567890.0123456789") }
-
- # SELECT
- row = Person.find(:first)
- assert_kind_of BigDecimal, row.wealth
+ def test_instance_based_migration_up
+ migration = MockMigration.new
+ assert !migration.went_up, 'have not gone up'
+ assert !migration.went_down, 'have not gone down'
- # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
+ migration.migrate :up
+ assert migration.went_up, 'have gone up'
+ assert !migration.went_down, 'have not gone down'
+ end
- # Reset to old state
- Person.connection.del_column "people", "wealth" rescue nil
- Person.reset_column_information
- end
+ def test_instance_based_migration_down
+ migration = MockMigration.new
+ assert !migration.went_up, 'have not gone up'
+ assert !migration.went_down, 'have not gone down'
- def test_add_column_with_precision_and_scale
- Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7
- Person.reset_column_information
+ migration.migrate :down
+ assert !migration.went_up, 'have gone up'
+ assert migration.went_down, 'have not gone down'
+ end
- wealth_column = Person.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ def test_migrator_one_up_with_exception_and_rollback
+ unless ActiveRecord::Base.connection.supports_ddl_transactions?
+ skip "not supported on #{ActiveRecord::Base.connection.class}"
end
- # Test SQLite adapter specifically for decimal types with precision and scale
- # attributes, since these need to be maintained in schema but aren't actually
- # used in SQLite itself
- if current_adapter?(:SQLite3Adapter)
- def test_change_column_with_new_precision_and_scale
- Person.delete_all
- Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7
- Person.reset_column_information
-
- Person.connection.change_column 'people', 'wealth', :decimal, :precision => 12, :scale => 8
- Person.reset_column_information
-
- wealth_column = Person.columns_hash['wealth']
- assert_equal 12, wealth_column.precision
- assert_equal 8, wealth_column.scale
- end
-
- def test_change_column_preserve_other_column_precision_and_scale
- Person.delete_all
- Person.connection.add_column 'people', 'last_name', :string
- Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7
- Person.reset_column_information
-
- wealth_column = Person.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
-
- Person.connection.change_column 'people', 'last_name', :string, :null => false
- Person.reset_column_information
-
- wealth_column = Person.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
- end
- end
+ refute Person.column_methods_hash.include?(:last_name)
- def test_native_types
- Person.delete_all
- Person.connection.add_column "people", "last_name", :string
- Person.connection.add_column "people", "bio", :text
- Person.connection.add_column "people", "age", :integer
- Person.connection.add_column "people", "height", :float
- Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10'
- Person.connection.add_column "people", "birthday", :datetime
- Person.connection.add_column "people", "favorite_day", :date
- Person.connection.add_column "people", "moment_of_truth", :datetime
- Person.connection.add_column "people", "male", :boolean
- Person.reset_column_information
-
- assert_nothing_raised do
- Person.create :first_name => 'bob', :last_name => 'bobsen',
- :bio => "I was born ....", :age => 18, :height => 1.78,
- :wealth => BigDecimal.new("12345678901234567890.0123456789"),
- :birthday => 18.years.ago, :favorite_day => 10.days.ago,
- :moment_of_truth => "1782-10-10 21:40:18", :male => true
- end
+ migration = Struct.new(:name, :version) {
+ def migrate(x); raise 'Something broke'; end
+ }.new('zomg', 100)
- bob = Person.find(:first)
- assert_equal 'bob', bob.first_name
- assert_equal 'bobsen', bob.last_name
- assert_equal "I was born ....", bob.bio
- assert_equal 18, bob.age
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- # Test for 30 significant digits (beyond the 16 of float), 10 of them
- # after the decimal place.
+ e = assert_raise(StandardError) { migrator.migrate }
- unless current_adapter?(:SQLite3Adapter)
- assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
- end
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
- assert_equal true, bob.male?
+ Person.reset_column_information
+ refute Person.column_methods_hash.include?(:last_name)
+ end
- assert_equal String, bob.first_name.class
- assert_equal String, bob.last_name.class
- assert_equal String, bob.bio.class
- assert_equal Fixnum, bob.age.class
- assert_equal Time, bob.birthday.class
+ def test_schema_migrations_table_name
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ Reminder.reset_table_name
+ assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ end
- if current_adapter?(:OracleAdapter, :SybaseAdapter)
- # Sybase, and Oracle don't differentiate between date/time
- assert_equal Time, bob.favorite_day.class
- else
- assert_equal Date, bob.favorite_day.class
- end
+ def test_proper_table_name
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
+ assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
+ Reminder.reset_table_name
+ assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+
+ # Use the model's own prefix/suffix if a model is given
+ ActiveRecord::Base.table_name_prefix = "ARprefix_"
+ ActiveRecord::Base.table_name_suffix = "_ARsuffix"
+ Reminder.table_name_prefix = 'prefix_'
+ Reminder.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ Reminder.table_name_prefix = ''
+ Reminder.table_name_suffix = ''
+ Reminder.reset_table_name
+
+ # Use AR::Base's prefix/suffix if string or symbol is given
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ Reminder.reset_table_name
+ end
- # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column
- # therefore no timezone change is done afterwards when default timezone is changed
- unless current_adapter?(:OracleAdapter)
- # Test DateTime column and defaults, including timezone.
- # FIXME: moment of truth may be Time on 64-bit platforms.
- if bob.moment_of_truth.is_a?(DateTime)
-
- with_env_tz 'US/Eastern' do
- bob.reload
- assert_equal DateTime.local_offset, bob.moment_of_truth.offset
- assert_not_equal 0, bob.moment_of_truth.offset
- assert_not_equal "Z", bob.moment_of_truth.zone
- # US/Eastern is -5 hours from GMT
- assert_equal Rational(-5, 24), bob.moment_of_truth.offset
- assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM
- assert_equal DateTime::ITALY, bob.moment_of_truth.start
- end
- end
- end
+ def test_rename_table_with_prefix_and_suffix
+ assert !Thing.table_exists?
+ ActiveRecord::Base.table_name_prefix = 'prefix_'
+ ActiveRecord::Base.table_name_suffix = '_suffix'
+ Thing.reset_table_name
+ Thing.reset_sequence_name
+ WeNeedThings.up
+
+ assert Thing.create("content" => "hello world")
+ assert_equal "hello world", Thing.find(:first).content
+
+ RenameThings.up
+ Thing.table_name = "prefix_awesome_things_suffix"
+
+ assert_equal "hello world", Thing.find(:first).content
+ ensure
+ ActiveRecord::Base.table_name_prefix = ''
+ ActiveRecord::Base.table_name_suffix = ''
+ Thing.reset_table_name
+ Thing.reset_sequence_name
+ end
- assert_instance_of TrueClass, bob.male?
- assert_kind_of BigDecimal, bob.wealth
- end
+ def test_add_drop_table_with_prefix_and_suffix
+ assert !Reminder.table_exists?
+ ActiveRecord::Base.table_name_prefix = 'prefix_'
+ ActiveRecord::Base.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ Reminder.reset_sequence_name
+ WeNeedReminders.up
+ assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
+ assert_equal "hello world", Reminder.find(:first).content
+
+ WeNeedReminders.down
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ ensure
+ ActiveRecord::Base.table_name_prefix = ''
+ ActiveRecord::Base.table_name_suffix = ''
+ Reminder.reset_table_name
+ Reminder.reset_sequence_name
+ end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- def test_unabstracted_database_dependent_types
- Person.delete_all
+ def test_create_table_with_binary_column
+ Person.connection.drop_table :binary_testings rescue nil
- ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint
- Person.reset_column_information
- assert_match(/tinyint/, Person.columns_hash['intelligence_quotient'].sql_type)
- ensure
- ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil
+ assert_nothing_raised {
+ Person.connection.create_table :binary_testings do |t|
+ t.column "data", :binary, :null => false
end
- end
-
- def test_add_remove_single_field_using_string_arguments
- assert !Person.column_methods_hash.include?(:last_name)
-
- ActiveRecord::Migration.add_column 'people', 'last_name', :string
+ }
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
-
- ActiveRecord::Migration.remove_column 'people', 'last_name'
-
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
- end
-
- def test_add_remove_single_field_using_symbol_arguments
- assert !Person.column_methods_hash.include?(:last_name)
-
- ActiveRecord::Migration.add_column :people, :last_name, :string
-
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
-
- ActiveRecord::Migration.remove_column :people, :last_name
-
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
- end
+ columns = Person.connection.columns(:binary_testings)
+ data_column = columns.detect { |c| c.name == "data" }
if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- def testing_table_for_positioning
- Person.connection.create_table :testings, :id => false do |t|
- t.column :first, :integer
- t.column :second, :integer
- t.column :third, :integer
- end
-
- yield Person.connection
- ensure
- Person.connection.drop_table :testings rescue nil
- end
- protected :testing_table_for_positioning
-
- def test_column_positioning
- testing_table_for_positioning do |conn|
- assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
- end
- end
-
- def test_add_column_with_positioning
- testing_table_for_positioning do |conn|
- conn.add_column :testings, :new_col, :integer
- assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
- end
- testing_table_for_positioning do |conn|
- conn.add_column :testings, :new_col, :integer, :first => true
- assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
- end
- testing_table_for_positioning do |conn|
- conn.add_column :testings, :new_col, :integer, :after => :first
- assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
- end
- end
-
- def test_change_column_with_positioning
- testing_table_for_positioning do |conn|
- conn.change_column :testings, :second, :integer, :first => true
- assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
- end
- testing_table_for_positioning do |conn|
- conn.change_column :testings, :second, :integer, :after => :third
- assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
- end
- end
- end
-
- def test_add_rename
- Person.delete_all
-
- begin
- Person.connection.add_column "people", "girlfriend", :string
- Person.reset_column_information
- Person.create :girlfriend => 'bobette'
-
- Person.connection.rename_column "people", "girlfriend", "exgirlfriend"
-
- Person.reset_column_information
- bob = Person.find(:first)
-
- assert_equal "bobette", bob.exgirlfriend
- ensure
- Person.connection.remove_column("people", "girlfriend") rescue nil
- Person.connection.remove_column("people", "exgirlfriend") rescue nil
- end
-
- end
-
- def test_rename_column_using_symbol_arguments
- begin
- names_before = Person.find(:all).map(&:first_name)
- Person.connection.rename_column :people, :first_name, :nick_name
- Person.reset_column_information
- assert Person.column_names.include?("nick_name")
- assert_equal names_before, Person.find(:all).map(&:nick_name)
- ensure
- Person.connection.remove_column("people","nick_name")
- Person.connection.add_column("people","first_name", :string)
- end
- end
-
- def test_rename_column
- begin
- names_before = Person.find(:all).map(&:first_name)
- Person.connection.rename_column "people", "first_name", "nick_name"
- Person.reset_column_information
- assert Person.column_names.include?("nick_name")
- assert_equal names_before, Person.find(:all).map(&:nick_name)
- ensure
- Person.connection.remove_column("people","nick_name")
- Person.connection.add_column("people","first_name", :string)
- end
+ assert_equal '', data_column.default
+ else
+ assert_nil data_column.default
end
- def test_rename_column_preserves_default_value_not_null
- begin
- default_before = Developer.connection.columns("developers").find { |c| c.name == "salary" }.default
- assert_equal 70000, default_before
- Developer.connection.rename_column "developers", "salary", "anual_salary"
- Developer.reset_column_information
- assert Developer.column_names.include?("anual_salary")
- default_after = Developer.connection.columns("developers").find { |c| c.name == "anual_salary" }.default
- assert_equal 70000, default_after
- ensure
- Developer.connection.rename_column "developers", "anual_salary", "salary"
- Developer.reset_column_information
- end
- end
-
- def test_rename_nonexistent_column
- ActiveRecord::Base.connection.create_table(:hats) do |table|
- table.column :hat_name, :string, :default => nil
- end
- exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- ActiveRecord::StatementInvalid
- else
- ActiveRecord::ActiveRecordError
- end
- assert_raise(exception) do
- Person.connection.rename_column "hats", "nonexistent", "should_fail"
- end
- ensure
- ActiveRecord::Base.connection.drop_table(:hats)
- end
-
- def test_rename_column_with_sql_reserved_word
- begin
- assert_nothing_raised { Person.connection.rename_column "people", "first_name", "group" }
- Person.reset_column_information
- assert Person.column_names.include?("group")
- ensure
- Person.connection.remove_column("people", "group") rescue nil
- Person.connection.add_column("people", "first_name", :string) rescue nil
- end
- end
-
- def test_rename_column_with_an_index
- ActiveRecord::Base.connection.create_table(:hats) do |table|
- table.column :hat_name, :string, :limit => 100
- table.column :hat_size, :integer
- end
- Person.connection.add_index :hats, :hat_name
- assert_nothing_raised do
- Person.connection.rename_column "hats", "hat_name", "name"
- end
- ensure
- ActiveRecord::Base.connection.drop_table(:hats)
- end
-
- def test_remove_column_with_index
- ActiveRecord::Base.connection.create_table(:hats) do |table|
- table.column :hat_name, :string, :limit => 100
- table.column :hat_size, :integer
- end
- ActiveRecord::Base.connection.add_index "hats", "hat_size"
-
- assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") }
- ensure
- ActiveRecord::Base.connection.drop_table(:hats)
- end
-
- def test_remove_column_with_multi_column_index
- ActiveRecord::Base.connection.create_table(:hats) do |table|
- table.column :hat_name, :string, :limit => 100
- table.column :hat_size, :integer
- table.column :hat_style, :string, :limit => 100
- end
- ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true
-
- assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") }
- ensure
- ActiveRecord::Base.connection.drop_table(:hats)
- end
-
- def test_remove_column_no_second_parameter_raises_exception
- assert_raise(ArgumentError) { Person.connection.remove_column("funny") }
- end
-
- def test_change_type_of_not_null_column
- assert_nothing_raised do
- Topic.connection.change_column "topics", "written_on", :datetime, :null => false
- Topic.reset_column_information
-
- Topic.connection.change_column "topics", "written_on", :datetime, :null => false
- Topic.reset_column_information
-
- Topic.connection.change_column "topics", "written_on", :datetime, :null => true
- Topic.reset_column_information
- end
- end
+ Person.connection.drop_table :binary_testings rescue nil
+ end
- if current_adapter?(:SQLite3Adapter)
- def test_rename_table_for_sqlite_should_work_with_reserved_words
- begin
- assert_nothing_raised do
- ActiveRecord::Base.connection.rename_table :references, :old_references
- ActiveRecord::Base.connection.create_table :octopuses do |t|
- t.column :url, :string
- end
- end
-
- assert_nothing_raised { ActiveRecord::Base.connection.rename_table :octopuses, :references }
-
- # Using explicit id in insert for compatibility across all databases
- con = ActiveRecord::Base.connection
- assert_nothing_raised do
- con.execute "INSERT INTO 'references' (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://rubyonrails.com')"
- end
- assert_equal 'http://rubyonrails.com', ActiveRecord::Base.connection.select_value("SELECT url FROM 'references' WHERE id=1")
-
- ensure
- ActiveRecord::Base.connection.drop_table :references
- ActiveRecord::Base.connection.rename_table :old_references, :references
- end
- end
- end
+ def test_create_table_with_custom_sequence_name
+ skip "not supported" unless current_adapter? :OracleAdapter
- def test_rename_table
+ # table name is 29 chars, the standard sequence name will
+ # be 33 chars and should be shortened
+ assert_nothing_raised do
begin
- ActiveRecord::Base.connection.create_table :octopuses do |t|
- t.column :url, :string
+ Person.connection.create_table :table_with_name_thats_just_ok do |t|
+ t.column :foo, :string, :null => false
end
- ActiveRecord::Base.connection.rename_table :octopuses, :octopi
-
- # Using explicit id in insert for compatibility across all databases
- con = ActiveRecord::Base.connection
- con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
- assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" }
- con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
-
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1")
-
ensure
- ActiveRecord::Base.connection.drop_table :octopuses rescue nil
- ActiveRecord::Base.connection.drop_table :octopi rescue nil
+ Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
end
end
- def test_change_column_nullability
- Person.delete_all
- Person.connection.add_column "people", "funny", :boolean
- Person.reset_column_information
- assert Person.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
- Person.connection.change_column "people", "funny", :boolean, :null => false, :default => true
- Person.reset_column_information
- assert !Person.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
- Person.connection.change_column "people", "funny", :boolean, :null => true
- Person.reset_column_information
- assert Person.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
- end
-
- def test_rename_table_with_an_index
+ # should be all good w/ a custom sequence name
+ assert_nothing_raised do
begin
- ActiveRecord::Base.connection.create_table :octopuses do |t|
- t.column :url, :string
+ Person.connection.create_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' do |t|
+ t.column :foo, :string, :null => false
end
- ActiveRecord::Base.connection.add_index :octopuses, :url
- ActiveRecord::Base.connection.rename_table :octopuses, :octopi
-
- # Using explicit id in insert for compatibility across all databases
- con = ActiveRecord::Base.connection
- con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
- assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" }
- con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1")
- assert ActiveRecord::Base.connection.indexes(:octopi).first.columns.include?("url")
ensure
- ActiveRecord::Base.connection.drop_table :octopuses rescue nil
- ActiveRecord::Base.connection.drop_table :octopi rescue nil
- end
- end
-
- def test_change_column
- Person.connection.add_column 'people', 'age', :integer
- label = "test_change_column Columns"
- old_columns = Person.connection.columns(Person.table_name, label)
- assert old_columns.find { |c| c.name == 'age' and c.type == :integer }
-
- assert_nothing_raised { Person.connection.change_column "people", "age", :string }
-
- new_columns = Person.connection.columns(Person.table_name, label)
- assert_nil new_columns.find { |c| c.name == 'age' and c.type == :integer }
- assert new_columns.find { |c| c.name == 'age' and c.type == :string }
-
- old_columns = Topic.connection.columns(Topic.table_name, label)
- assert old_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
- assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => false }
- new_columns = Topic.connection.columns(Topic.table_name, label)
- assert_nil new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
- assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
- assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => true }
- end
-
- def test_change_column_with_nil_default
- Person.connection.add_column "people", "contributor", :boolean, :default => true
- Person.reset_column_information
- assert Person.new.contributor?
-
- assert_nothing_raised { Person.connection.change_column "people", "contributor", :boolean, :default => nil }
- Person.reset_column_information
- assert !Person.new.contributor?
- assert_nil Person.new.contributor
- ensure
- Person.connection.remove_column("people", "contributor") rescue nil
- end
-
- def test_change_column_with_new_default
- Person.connection.add_column "people", "administrator", :boolean, :default => true
- Person.reset_column_information
- assert Person.new.administrator?
-
- assert_nothing_raised { Person.connection.change_column "people", "administrator", :boolean, :default => false }
- Person.reset_column_information
- assert !Person.new.administrator?
- ensure
- Person.connection.remove_column("people", "administrator") rescue nil
- end
-
- def test_change_column_default
- Person.connection.change_column_default "people", "first_name", "Tester"
- Person.reset_column_information
- assert_equal "Tester", Person.new.first_name
- end
-
- def test_change_column_quotes_column_names
- Person.connection.create_table :testings do |t|
- t.column :select, :string
- end
-
- assert_nothing_raised { Person.connection.change_column :testings, :select, :string, :limit => 10 }
-
- # Oracle needs primary key value from sequence
- if current_adapter?(:OracleAdapter)
- assert_nothing_raised { Person.connection.execute "insert into testings (id, #{Person.connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')" }
- else
- assert_nothing_raised { Person.connection.execute "insert into testings (#{Person.connection.quote_column_name('select')}) values ('7 chars')" }
- end
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_keeping_default_and_notnull_constaint_on_change
- Person.connection.create_table :testings do |t|
- t.column :title, :string
- end
- person_klass = Class.new(Person)
- person_klass.set_table_name 'testings'
-
- person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
- person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
- assert_equal false, person_klass.columns_hash["wealth"].null
- # Oracle needs primary key value from sequence
- if current_adapter?(:OracleAdapter)
- assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")}
- else
- assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")}
- end
-
- # change column default to see that column doesn't lose its not null definition
- person_klass.connection.change_column_default "testings", "wealth", 100
- person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
- assert_equal false, person_klass.columns_hash["wealth"].null
-
- # rename column to see that column doesn't lose its not null and/or default definition
- person_klass.connection.rename_column "testings", "wealth", "money"
- person_klass.reset_column_information
- assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
- assert_equal false, person_klass.columns_hash["money"].null
-
- # change column
- person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
- person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
- assert_equal false, person_klass.columns_hash["money"].null
-
- # change column, make it nullable and clear default
- person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil
- person_klass.reset_column_information
- assert_nil person_klass.columns_hash["money"].default
- assert_equal true, person_klass.columns_hash["money"].null
-
- # change_column_null, make it not nullable and set null values to a default value
- person_klass.connection.execute('UPDATE testings SET money = NULL')
- person_klass.connection.change_column_null "testings", "money", false, 2000
- person_klass.reset_column_information
- assert_nil person_klass.columns_hash["money"].default
- assert_equal false, person_klass.columns_hash["money"].null
- assert_equal [2000], Person.connection.select_values("SELECT money FROM testings").map { |s| s.to_i }.sort
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_change_column_default_to_null
- Person.connection.change_column_default "people", "first_name", nil
- Person.reset_column_information
- assert_nil Person.new.first_name
- end
-
- def test_column_exists
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
+ Person.connection.drop_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' rescue nil
end
-
- assert Person.connection.column_exists?(:testings, :foo)
- assert !Person.connection.column_exists?(:testings, :bar)
- ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_column_exists_with_type
- Person.connection.create_table :testings do |t|
- t.column :foo, :string
- t.column :bar, :decimal, :precision => 8, :scale => 2
- end
-
- assert Person.connection.column_exists?(:testings, :foo, :string)
- assert !Person.connection.column_exists?(:testings, :foo, :integer)
- assert Person.connection.column_exists?(:testings, :bar, :decimal)
- assert !Person.connection.column_exists?(:testings, :bar, :integer)
- ensure
- Person.connection.drop_table :testings rescue nil
end
- def test_column_exists_with_definition
- Person.connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- t.column :bar, :decimal, :precision => 8, :scale => 2
- end
-
- assert Person.connection.column_exists?(:testings, :foo, :string, :limit => 100)
- assert !Person.connection.column_exists?(:testings, :foo, :string, :limit => 50)
- assert Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2)
- assert !Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2)
- ensure
- Person.connection.drop_table :testings rescue nil
+ # confirm the custom sequence got dropped
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
end
+ 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
+ protected
+ def with_env_tz(new_tz = 'US/Eastern')
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ yield
ensure
- Person.connection.drop_table :testings rescue nil
- end
-
- def test_add_table
- assert !Reminder.table_exists?
-
- WeNeedReminders.up
-
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert_equal "hello world", Reminder.find(:first).content
-
- WeNeedReminders.down
- assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
- end
-
- def test_add_table_with_decimals
- Person.connection.drop_table :big_numbers rescue nil
-
- assert !BigNumber.table_exists?
- GiveMeBigNumbers.up
-
- assert BigNumber.create(
- :bank_balance => 1586.43,
- :big_bank_balance => BigDecimal("1000234000567.95"),
- :world_population => 6000000000,
- :my_house_population => 3,
- :value_of_e => BigDecimal("2.7182818284590452353602875")
- )
-
- b = BigNumber.find(:first)
- assert_not_nil b
-
- assert_not_nil b.bank_balance
- assert_not_nil b.big_bank_balance
- assert_not_nil b.world_population
- assert_not_nil b.my_house_population
- assert_not_nil b.value_of_e
-
- # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
- # is_a?(Bignum)
- assert_kind_of Integer, b.world_population
- assert_equal 6000000000, b.world_population
- assert_kind_of Fixnum, b.my_house_population
- assert_equal 3, b.my_house_population
- assert_kind_of BigDecimal, b.bank_balance
- assert_equal BigDecimal("1586.43"), b.bank_balance
- assert_kind_of BigDecimal, b.big_bank_balance
- assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance
-
- # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with
- # precision/scale explicitly left out. By the SQL standard, numbers
- # assigned to this field should be truncated but that's seldom respected.
- if current_adapter?(:PostgreSQLAdapter)
- # - PostgreSQL changes the SQL spec on columns declared simply as
- # "decimal" to something more useful: instead of being given a scale
- # of 0, they take on the compile-time limit for precision and scale,
- # so the following should succeed unless you have used really wacky
- # compilation options
- # - SQLite2 has the default behavior of preserving all data sent in,
- # so this happens there too
- assert_kind_of BigDecimal, b.value_of_e
- assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e
- elsif current_adapter?(:SQLite3Adapter)
- # - SQLite3 stores a float, in violation of SQL
- assert_kind_of BigDecimal, b.value_of_e
- assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001
- else
- # - SQL standard is an integer
- assert_kind_of Fixnum, b.value_of_e
- assert_equal 2, b.value_of_e
- end
-
- GiveMeBigNumbers.down
- assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
+end
- def test_migrator
- assert !Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
-
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid")
-
- assert_equal 3, ActiveRecord::Migrator.current_version
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert_equal "hello world", Reminder.find(:first).content
-
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
-
- assert_equal 0, ActiveRecord::Migrator.current_version
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
- assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+class ReservedWordsMigrationTest < ActiveRecord::TestCase
+ def test_drop_index_from_table_named_values
+ connection = Person.connection
+ connection.create_table :values, :force => true do |t|
+ t.integer :value
end
- class MockMigration < ActiveRecord::Migration
- attr_reader :went_up, :went_down
- def initialize
- @went_up = false
- @went_down = false
- end
-
- def up
- @went_up = true
- super
- end
-
- def down
- @went_down = true
- super
- end
+ assert_nothing_raised do
+ connection.add_index :values, :value
+ connection.remove_index :values, :column => :value
end
- def test_instance_based_migration_up
- migration = MockMigration.new
- assert !migration.went_up, 'have not gone up'
- assert !migration.went_down, 'have not gone down'
-
- migration.migrate :up
- assert migration.went_up, 'have gone up'
- assert !migration.went_down, 'have not gone down'
- end
+ connection.drop_table :values rescue nil
+ end
+end
- def test_instance_based_migration_down
- migration = MockMigration.new
- assert !migration.went_up, 'have not gone up'
- assert !migration.went_down, 'have not gone down'
- migration.migrate :down
- assert !migration.went_up, 'have gone up'
- assert migration.went_down, 'have not gone down'
+class ChangeTableMigrationsTest < ActiveRecord::TestCase
+ def setup
+ @connection = Person.connection
+ @connection.create_table :delete_me, :force => true do |t|
end
+ end
- def test_migrator_one_up
- assert !Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
-
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
-
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
-
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 2)
+ def teardown
+ Person.connection.drop_table :delete_me rescue nil
+ end
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert_equal "hello world", Reminder.find(:first).content
+ def test_references_column_type_adds_id
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.references :customer
end
+ end
- def test_migrator_one_down
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid")
-
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 1)
-
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
+ def test_remove_references_column_type_removes_id
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_references :customer
end
+ end
- def test_migrator_one_up_one_down
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0)
-
- assert !Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
+ def test_add_belongs_to_works_like_add_references
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.belongs_to :customer
end
+ end
- def test_migrator_double_up
- assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1)
- assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) }
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ def test_remove_belongs_to_works_like_remove_references
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_belongs_to :customer
end
+ end
- def test_migrator_double_down
- assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1)
- ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1)
- assert_nothing_raised { ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1) }
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ def test_references_column_type_with_polymorphic_adds_type
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {})
+ t.references :taggable, :polymorphic => true
end
+ end
- if ActiveRecord::Base.connection.supports_ddl_transactions?
- def test_migrator_one_up_with_exception_and_rollback
- assert !Person.column_methods_hash.include?(:last_name)
-
- e = assert_raise(StandardError) do
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
- end
-
- assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
-
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
- end
+ def test_remove_references_column_type_with_polymorphic_removes_type
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true
end
+ end
- def test_finds_migrations
- migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
-
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
- assert_equal migrations[i].version, pair.first
- assert_equal migrations[i].name, pair.last
- end
+ def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false})
+ t.references :taggable, :polymorphic => true, :null => false
end
+ end
- def test_finds_migrations_from_two_directories
- directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
- migrations = ActiveRecord::Migrator.new(:up, directories).migrations
-
- [[20090101010101, "PeopleHaveHobbies"],
- [20090101010202, "PeopleHaveDescriptions"],
- [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
- [20100201010101, "ValidWithTimestampsWeNeedReminders"],
- [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
- assert_equal pair.first, migrations[i].version
- assert_equal pair.last, migrations[i].name
- end
+ def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true, :null => false
end
+ end
- def test_dump_schema_information_outputs_lexically_ordered_versions
- migration_path = MIGRATIONS_ROOT + '/valid_with_timestamps'
- ActiveRecord::Migrator.run(:up, migration_path, 20100301010101)
- ActiveRecord::Migrator.run(:up, migration_path, 20100201010101)
-
- schema_info = ActiveRecord::Base.connection.dump_schema_information
- assert_match(/20100201010101.*20100301010101/m, schema_info)
+ def test_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:add_timestamps).with(:delete_me)
+ t.timestamps
end
+ end
- def test_finds_pending_migrations
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1)
- migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations
-
- assert_equal 1, migrations.size
- assert_equal migrations[0].version, 3
- assert_equal migrations[0].name, 'InterleavedInnocentJointable'
+ def test_remove_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:remove_timestamps).with(:delete_me)
+ t.remove_timestamps
end
+ end
- def test_relative_migrations
- list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.up("valid/", 1)
- end
-
- migration_proxy = list.find { |item|
- item.name == 'ValidPeopleHaveLastNames'
- }
- assert migration_proxy, 'should find pending migration'
+ def string_column
+ if current_adapter?(:PostgreSQLAdapter)
+ "character varying(255)"
+ elsif current_adapter?(:OracleAdapter)
+ 'VARCHAR2(255)'
+ else
+ 'varchar(255)'
end
+ end
- def test_only_loads_pending_migrations
- # migrate up to 1
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
-
- proxies = ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil)
-
- names = proxies.map(&:name)
- assert !names.include?('ValidPeopleHaveLastNames')
- assert names.include?('WeNeedReminders')
- assert names.include?('InnocentJointable')
+ def integer_column
+ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ 'int(11)'
+ elsif current_adapter?(:OracleAdapter)
+ 'NUMBER(38)'
+ else
+ 'integer'
end
+ end
- def test_target_version_zero_should_run_only_once
- # migrate up to 1
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1)
-
- # migrate down to 0
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
-
- # migrate down to 0 again
- proxies = ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
- assert_equal [], proxies
+ def test_integer_creates_integer_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {})
+ t.integer :foo, :bar
end
+ end
- def test_migrator_db_has_no_schema_migrations_table
- # Oracle adapter raises error if semicolon is present as last character
- if current_adapter?(:OracleAdapter)
- ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
- else
- ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations;")
- end
- assert_nothing_raised do
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1)
- end
- end
-
- def test_migrator_verbosity
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
- assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migration.message_count = 0
-
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0)
- assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migration.message_count = 0
+ def test_string_creates_string_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, string_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, string_column, {})
+ t.string :foo, :bar
end
+ end
- def test_migrator_verbosity_off
- ActiveRecord::Migration.verbose = false
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
- assert_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0)
- assert_equal 0, ActiveRecord::Migration.message_count
+ def test_column_creates_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {})
+ t.column :bar, :integer
end
+ end
- def test_migrator_going_down_due_to_version_target
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
-
- assert !Person.column_methods_hash.include?(:last_name)
- assert !Reminder.table_exists?
-
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
-
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert_equal "hello world", Reminder.find(:first).content
+ def test_column_creates_column_with_options
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false})
+ t.column :bar, :integer, :null => false
end
+ end
- def test_migrator_rollback
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ def test_index_creates_index
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {})
+ t.index :bar
end
+ end
- def test_migrator_forward
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
-
- ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ def test_index_creates_index_with_options
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true})
+ t.index :bar, :unique => true
end
+ end
- def test_get_all_versions
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
- assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
-
- ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ def test_index_exists
+ with_change_table do |t|
+ @connection.expects(:index_exists?).with(:delete_me, :bar, {})
+ t.index_exists?(:bar)
end
+ end
- def test_schema_migrations_table_name
- ActiveRecord::Base.table_name_prefix = "prefix_"
- ActiveRecord::Base.table_name_suffix = "_suffix"
- Reminder.reset_table_name
- assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
- Reminder.reset_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
- ensure
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
- end
-
- def test_proper_table_name
- assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
- assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
- Reminder.reset_table_name
- assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
-
- # Use the model's own prefix/suffix if a model is given
- ActiveRecord::Base.table_name_prefix = "ARprefix_"
- ActiveRecord::Base.table_name_suffix = "_ARsuffix"
- Reminder.table_name_prefix = 'prefix_'
- Reminder.table_name_suffix = '_suffix'
- Reminder.reset_table_name
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
- Reminder.table_name_prefix = ''
- Reminder.table_name_suffix = ''
- Reminder.reset_table_name
-
- # Use AR::Base's prefix/suffix if string or symbol is given
- ActiveRecord::Base.table_name_prefix = "prefix_"
- ActiveRecord::Base.table_name_suffix = "_suffix"
- Reminder.reset_table_name
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
- Reminder.reset_table_name
- end
-
- def test_add_drop_table_with_prefix_and_suffix
- assert !Reminder.table_exists?
- ActiveRecord::Base.table_name_prefix = 'prefix_'
- ActiveRecord::Base.table_name_suffix = '_suffix'
- Reminder.reset_table_name
- Reminder.reset_sequence_name
- WeNeedReminders.up
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert_equal "hello world", Reminder.find(:first).content
-
- WeNeedReminders.down
- assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
- ensure
- ActiveRecord::Base.table_name_prefix = ''
- ActiveRecord::Base.table_name_suffix = ''
- Reminder.reset_table_name
- Reminder.reset_sequence_name
+ def test_index_exists_with_options
+ with_change_table do |t|
+ @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true})
+ t.index_exists?(:bar, :unique => true)
end
+ end
- def test_create_table_with_binary_column
- Person.connection.drop_table :binary_testings rescue nil
-
- assert_nothing_raised {
- Person.connection.create_table :binary_testings do |t|
- t.column "data", :binary, :null => false
- end
- }
-
- columns = Person.connection.columns(:binary_testings)
- data_column = columns.detect { |c| c.name == "data" }
-
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- assert_equal '', data_column.default
- else
- assert_nil data_column.default
- end
-
- Person.connection.drop_table :binary_testings rescue nil
+ def test_change_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {})
+ t.change :bar, :string
end
+ end
- def test_migrator_with_duplicates
- assert_raise(ActiveRecord::DuplicateMigrationVersionError) do
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil)
- end
+ def test_change_changes_column_with_options
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true})
+ t.change :bar, :string, :null => true
end
+ end
- def test_migrator_with_duplicate_names
- assert_raise(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate_names", nil)
- end
+ def test_change_default_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column_default).with(:delete_me, :bar, :string)
+ t.change_default :bar, :string
end
+ end
- def test_migrator_with_missing_version_numbers
- assert_raise(ActiveRecord::UnknownMigrationVersionError) do
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 500)
- end
+ def test_remove_drops_single_column
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar])
+ t.remove :bar
end
+ end
- def test_create_table_with_custom_sequence_name
- return unless current_adapter? :OracleAdapter
-
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
- end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
- end
- end
-
- # should be all good w/ a custom sequence name
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' do |t|
- t.column :foo, :string, :null => false
- end
-
- Person.connection.execute("select suitably_short_seq.nextval from dual")
-
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' rescue nil
- end
- end
-
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
- end
+ def test_remove_drops_multiple_columns
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar, :baz])
+ t.remove :bar, :baz
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
end
- class MigrationLoggerTest < ActiveRecord::TestCase
- def test_migration_should_be_run_without_logger
- previous_logger = ActiveRecord::Base.logger
- ActiveRecord::Base.logger = nil
- assert_nothing_raised do
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
- end
- ensure
- ActiveRecord::Base.logger = previous_logger
+ def test_remove_index_removes_index_with_options
+ with_change_table do |t|
+ @connection.expects(:remove_index).with(:delete_me, {:unique => true})
+ t.remove_index :unique => true
end
end
- class InterleavedMigrationsTest < ActiveRecord::TestCase
- def test_migrator_interleaved_migrations
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1")
-
- assert_nothing_raised do
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2")
- end
-
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
-
- assert_nothing_raised do
- proxies = ActiveRecord::Migrator.down(
- MIGRATIONS_ROOT + "/interleaved/pass_3")
- names = proxies.map(&:name)
- assert names.include?('InterleavedPeopleHaveLastNames')
- assert names.include?('InterleavedInnocentJointable')
- end
+ def test_rename_renames_column
+ with_change_table do |t|
+ @connection.expects(:rename_column).with(:delete_me, :bar, :baz)
+ t.rename :bar, :baz
end
end
- class ReservedWordsMigrationTest < ActiveRecord::TestCase
- def test_drop_index_from_table_named_values
- connection = Person.connection
- connection.create_table :values, :force => true do |t|
- t.integer :value
- end
-
- assert_nothing_raised do
- connection.add_index :values, :value
- connection.remove_index :values, :column => :value
- end
-
- connection.drop_table :values rescue nil
+ protected
+ def with_change_table
+ Person.connection.change_table :delete_me do |t|
+ yield t
end
end
+end
-
- class ChangeTableMigrationsTest < ActiveRecord::TestCase
+if ActiveRecord::Base.connection.supports_bulk_alter?
+ class BulkAlterTableMigrationsTest < ActiveRecord::TestCase
def setup
@connection = Person.connection
- @connection.create_table :delete_me, :force => true do |t|
- end
+ @connection.create_table(:delete_me, :force => true) {|t| }
end
def teardown
- Person.connection.drop_table :delete_me rescue nil
- end
-
- def test_references_column_type_adds_id
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
- t.references :customer
- end
+ Person.connection.drop_table(:delete_me) rescue nil
end
- def test_remove_references_column_type_removes_id
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'customer_id')
- t.remove_references :customer
+ def test_adding_multiple_columns
+ assert_queries(1) do
+ with_bulk_change_table do |t|
+ t.column :name, :string
+ t.string :qualification, :experience
+ t.integer :age, :default => 0
+ t.date :birthdate
+ t.timestamps
+ end
end
- end
- def test_add_belongs_to_works_like_add_references
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
- t.belongs_to :customer
- end
+ assert_equal 8, columns.size
+ [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
+ assert_equal 0, column(:age).default
end
- def test_remove_belongs_to_works_like_remove_references
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'customer_id')
- t.remove_belongs_to :customer
+ def test_removing_columns
+ with_bulk_change_table do |t|
+ t.string :qualification, :experience
end
- end
- def test_references_column_type_with_polymorphic_adds_type
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {})
- @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {})
- t.references :taggable, :polymorphic => true
- end
- end
+ [:qualification, :experience].each {|c| assert column(c) }
- def test_remove_references_column_type_with_polymorphic_removes_type
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
- @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
- t.remove_references :taggable, :polymorphic => true
+ assert_queries(1) do
+ with_bulk_change_table do |t|
+ t.remove :qualification, :experience
+ t.string :qualification_experience
+ end
end
- end
- def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false})
- @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false})
- t.references :taggable, :polymorphic => true, :null => false
- end
+ [:qualification, :experience].each {|c| assert ! column(c) }
+ assert column(:qualification_experience)
end
- def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
- @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
- t.remove_references :taggable, :polymorphic => true, :null => false
+ def test_adding_indexes
+ with_bulk_change_table do |t|
+ t.string :username
+ t.string :name
+ t.integer :age
end
- end
- def test_timestamps_creates_updated_at_and_created_at
- with_change_table do |t|
- @connection.expects(:add_timestamps).with(:delete_me)
- t.timestamps
+ # Adding an index fires a query every time to check if an index already exists or not
+ assert_queries(3) do
+ with_bulk_change_table do |t|
+ t.index :username, :unique => true, :name => :awesome_username_index
+ t.index [:name, :age]
+ end
end
- end
- def test_remove_timestamps_creates_updated_at_and_created_at
- with_change_table do |t|
- @connection.expects(:remove_timestamps).with(:delete_me)
- t.remove_timestamps
- end
- end
+ assert_equal 2, indexes.size
- def string_column
- if current_adapter?(:PostgreSQLAdapter)
- "character varying(255)"
- elsif current_adapter?(:OracleAdapter)
- 'VARCHAR2(255)'
- else
- 'varchar(255)'
- end
- end
+ name_age_index = index(:index_delete_me_on_name_and_age)
+ assert_equal ['name', 'age'].sort, name_age_index.columns.sort
+ assert ! name_age_index.unique
- def integer_column
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- 'int(11)'
- elsif current_adapter?(:OracleAdapter)
- 'NUMBER(38)'
- else
- 'integer'
- end
+ assert index(:awesome_username_index).unique
end
- def test_integer_creates_integer_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {})
- @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {})
- t.integer :foo, :bar
+ def test_removing_index
+ with_bulk_change_table do |t|
+ t.string :name
+ t.index :name
end
- end
- def test_string_creates_string_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :foo, string_column, {})
- @connection.expects(:add_column).with(:delete_me, :bar, string_column, {})
- t.string :foo, :bar
- end
- end
+ assert index(:index_delete_me_on_name)
- def test_column_creates_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :bar, :integer, {})
- t.column :bar, :integer
+ assert_queries(3) do
+ with_bulk_change_table do |t|
+ t.remove_index :name
+ t.index :name, :name => :new_name_index, :unique => true
+ end
end
- end
- def test_column_creates_column_with_options
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false})
- t.column :bar, :integer, :null => false
- end
- end
+ assert ! index(:index_delete_me_on_name)
- def test_index_creates_index
- with_change_table do |t|
- @connection.expects(:add_index).with(:delete_me, :bar, {})
- t.index :bar
- end
+ new_name_index = index(:new_name_index)
+ assert new_name_index.unique
end
- def test_index_creates_index_with_options
- with_change_table do |t|
- @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true})
- t.index :bar, :unique => true
+ def test_changing_columns
+ with_bulk_change_table do |t|
+ t.string :name
+ t.date :birthdate
end
- end
- def test_index_exists
- with_change_table do |t|
- @connection.expects(:index_exists?).with(:delete_me, :bar, {})
- t.index_exists?(:bar)
- end
- end
+ assert ! column(:name).default
+ assert_equal :date, column(:birthdate).type
- def test_index_exists_with_options
- with_change_table do |t|
- @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true})
- t.index_exists?(:bar, :unique => true)
+ # One query for columns (delete_me table)
+ # One query for primary key (delete_me table)
+ # One query to do the bulk change
+ assert_queries(3) do
+ with_bulk_change_table do |t|
+ t.change :name, :string, :default => 'NONAME'
+ t.change :birthdate, :datetime
+ end
end
- end
- def test_change_changes_column
- with_change_table do |t|
- @connection.expects(:change_column).with(:delete_me, :bar, :string, {})
- t.change :bar, :string
- end
+ assert_equal 'NONAME', column(:name).default
+ assert_equal :datetime, column(:birthdate).type
end
- def test_change_changes_column_with_options
- with_change_table do |t|
- @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true})
- t.change :bar, :string, :null => true
- end
- end
+ protected
- def test_change_default_changes_column
- with_change_table do |t|
- @connection.expects(:change_column_default).with(:delete_me, :bar, :string)
- t.change_default :bar, :string
- end
- end
+ def with_bulk_change_table
+ # Reset columns/indexes cache as we're changing the table
+ @columns = @indexes = nil
- def test_remove_drops_single_column
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, [:bar])
- t.remove :bar
+ Person.connection.change_table(:delete_me, :bulk => true) do |t|
+ yield t
end
end
- def test_remove_drops_multiple_columns
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, [:bar, :baz])
- t.remove :bar, :baz
- end
+ def column(name)
+ columns.detect {|c| c.name == name.to_s }
end
- def test_remove_index_removes_index_with_options
- with_change_table do |t|
- @connection.expects(:remove_index).with(:delete_me, {:unique => true})
- t.remove_index :unique => true
- end
+ def columns
+ @columns ||= Person.connection.columns('delete_me')
end
- def test_rename_renames_column
- with_change_table do |t|
- @connection.expects(:rename_column).with(:delete_me, :bar, :baz)
- t.rename :bar, :baz
- end
+ def index(name)
+ indexes.detect {|i| i.name == name.to_s }
end
- protected
- def with_change_table
- Person.connection.change_table :delete_me do |t|
- yield t
- end
+ def indexes
+ @indexes ||= Person.connection.indexes('delete_me')
end
- end
-
- if ActiveRecord::Base.connection.supports_bulk_alter?
- class BulkAlterTableMigrationsTest < ActiveRecord::TestCase
- def setup
- @connection = Person.connection
- @connection.create_table(:delete_me, :force => true) {|t| }
- end
-
- def teardown
- Person.connection.drop_table(:delete_me) rescue nil
- end
-
- def test_adding_multiple_columns
- assert_queries(1) do
- with_bulk_change_table do |t|
- t.column :name, :string
- t.string :qualification, :experience
- t.integer :age, :default => 0
- t.date :birthdate
- t.timestamps
- end
- end
-
- assert_equal 8, columns.size
- [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
- end
-
- def test_removing_columns
- with_bulk_change_table do |t|
- t.string :qualification, :experience
- end
+ end # AlterTableMigrationsTest
- [:qualification, :experience].each {|c| assert column(c) }
-
- assert_queries(1) do
- with_bulk_change_table do |t|
- t.remove :qualification, :experience
- t.string :qualification_experience
- end
- end
-
- [:qualification, :experience].each {|c| assert ! column(c) }
- assert column(:qualification_experience)
- end
-
- def test_adding_indexes
- with_bulk_change_table do |t|
- t.string :username
- t.string :name
- t.integer :age
- end
-
- # Adding an index fires a query every time to check if an index already exists or not
- assert_queries(3) do
- with_bulk_change_table do |t|
- t.index :username, :unique => true, :name => :awesome_username_index
- t.index [:name, :age]
- end
- end
-
- assert_equal 2, indexes.size
-
- name_age_index = index(:index_delete_me_on_name_and_age)
- assert_equal ['name', 'age'].sort, name_age_index.columns.sort
- assert ! name_age_index.unique
-
- assert index(:awesome_username_index).unique
- end
-
- def test_removing_index
- with_bulk_change_table do |t|
- t.string :name
- t.index :name
- end
-
- assert index(:index_delete_me_on_name)
-
- assert_queries(3) do
- with_bulk_change_table do |t|
- t.remove_index :name
- t.index :name, :name => :new_name_index, :unique => true
- end
- end
-
- assert ! index(:index_delete_me_on_name)
-
- new_name_index = index(:new_name_index)
- assert new_name_index.unique
- end
-
- def test_changing_columns
- with_bulk_change_table do |t|
- t.string :name
- t.date :birthdate
- end
-
- assert ! column(:name).default
- assert_equal :date, column(:birthdate).type
-
- # One query for columns (delete_me table)
- # One query for primary key (delete_me table)
- # One query to do the bulk change
- assert_queries(3) do
- with_bulk_change_table do |t|
- t.change :name, :string, :default => 'NONAME'
- t.change :birthdate, :datetime
- end
- end
-
- assert_equal 'NONAME', column(:name).default
- assert_equal :datetime, column(:birthdate).type
- end
-
- protected
-
- def with_bulk_change_table
- # Reset columns/indexes cache as we're changing the table
- @columns = @indexes = nil
-
- Person.connection.change_table(:delete_me, :bulk => true) do |t|
- yield t
- end
- end
-
- def column(name)
- columns.detect {|c| c.name == name.to_s }
- end
-
- def columns
- @columns ||= Person.connection.columns('delete_me')
- end
-
- def index(name)
- indexes.detect {|i| i.name == name.to_s }
- end
+end
- def indexes
- @indexes ||= Person.connection.indexes('delete_me')
- end
- end # AlterTableMigrationsTest
+class CopyMigrationsTest < ActiveRecord::TestCase
+ def setup
+ end
+ def clear
+ ActiveRecord::Base.timestamped_migrations = true
+ to_delete = Dir[@migrations_path + "/*.rb"] - @existing_migrations
+ File.delete(*to_delete)
end
- class CopyMigrationsTest < ActiveRecord::TestCase
- def setup
- end
+ def test_copying_migrations_without_timestamps
+ ActiveRecord::Base.timestamped_migrations = false
+ @migrations_path = MIGRATIONS_ROOT + "/valid"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
+
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
+ assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename)
+
+ expected = "# This migration comes from bukkits (originally 1)"
+ assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[0].chomp
+
+ files_count = Dir[@migrations_path + "/*.rb"].length
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
+ assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
+ assert copied.empty?
+ ensure
+ clear
+ end
- def clear
- ActiveRecord::Base.timestamped_migrations = true
- to_delete = Dir[@migrations_path + "/*.rb"] - @existing_migrations
- File.delete(*to_delete)
- end
+ def test_copying_migrations_without_timestamps_from_2_sources
+ ActiveRecord::Base.timestamped_migrations = false
+ @migrations_path = MIGRATIONS_ROOT + "/valid"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
+
+ sources = ActiveSupport::OrderedHash.new
+ sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy"
+ sources[:omg] = MIGRATIONS_ROOT + "/to_copy2"
+ ActiveRecord::Migration.copy(@migrations_path, sources)
+ assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exists?(@migrations_path + "/6_create_articles.omg.rb")
+ assert File.exists?(@migrations_path + "/7_create_comments.omg.rb")
+
+ files_count = Dir[@migrations_path + "/*.rb"].length
+ ActiveRecord::Migration.copy(@migrations_path, sources)
+ assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
+ ensure
+ clear
+ end
- def test_copying_migrations_without_timestamps
- ActiveRecord::Base.timestamped_migrations = false
- @migrations_path = MIGRATIONS_ROOT + "/valid"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ def test_copying_migrations_with_timestamps
+ @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb")
- assert_equal [@migrations_path + "/4_people_have_hobbies.rb", @migrations_path + "/5_people_have_descriptions.rb"], copied.map(&:filename)
+ Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
+ @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"]
+ assert_equal expected, copied.map(&:filename)
files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
assert copied.empty?
- ensure
- clear
end
+ ensure
+ clear
+ end
- def test_copying_migrations_without_timestamps_from_2_sources
- ActiveRecord::Base.timestamped_migrations = false
- @migrations_path = MIGRATIONS_ROOT + "/valid"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ def test_copying_migrations_with_timestamps_from_2_sources
+ @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
- sources = ActiveSupport::OrderedHash.new
- sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy"
- sources[:omg] = MIGRATIONS_ROOT + "/to_copy2"
- ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb")
- assert File.exists?(@migrations_path + "/6_create_articles.rb")
- assert File.exists?(@migrations_path + "/7_create_comments.rb")
+ sources = ActiveSupport::OrderedHash.new
+ sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
+ sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2"
+
+ Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ copied = ActiveRecord::Migration.copy(@migrations_path, sources)
+ assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb")
+ assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb")
+ assert_equal 4, copied.length
files_count = Dir[@migrations_path + "/*.rb"].length
ActiveRecord::Migration.copy(@migrations_path, sources)
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- ensure
- clear
end
+ ensure
+ clear
+ end
- def test_copying_migrations_with_timestamps
- @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ def test_copying_migrations_with_timestamps_to_destination_with_timestamps_in_future
+ @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb")
- expected = [@migrations_path + "/20100726101010_people_have_hobbies.rb",
- @migrations_path + "/20100726101011_people_have_descriptions.rb"]
- assert_equal expected, copied.map(&:filename)
+ Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
+ ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
- files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
- end
- ensure
- clear
+ files_count = Dir[@migrations_path + "/*.rb"].length
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
+ assert copied.empty?
end
+ ensure
+ clear
+ end
- def test_copying_migrations_with_timestamps_from_2_sources
- @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ def test_skipping_migrations
+ @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
- sources = ActiveSupport::OrderedHash.new
- sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
- sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2"
+ sources = ActiveSupport::OrderedHash.new
+ sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
+ sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision"
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb")
- assert File.exists?(@migrations_path + "/20100726101012_create_articles.rb")
- assert File.exists?(@migrations_path + "/20100726101013_create_comments.rb")
- assert_equal 4, copied.length
+ skipped = []
+ on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" }
+ copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
+ assert_equal 2, copied.length
- files_count = Dir[@migrations_path + "/*.rb"].length
- ActiveRecord::Migration.copy(@migrations_path, sources)
- assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- end
- ensure
- clear
- end
+ assert_equal 1, skipped.length
+ assert_equal ["omg PeopleHaveHobbies"], skipped
+ ensure
+ clear
+ end
- def test_copying_migrations_with_timestamps_to_destination_with_timestamps_in_future
- @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ def test_skip_is_not_called_if_migrations_are_from_the_same_plugin
+ @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
- ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.rb")
+ sources = ActiveSupport::OrderedHash.new
+ sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
- files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
- end
- ensure
- clear
- end
+ skipped = []
+ on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" }
+ copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
+ ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
- def test_skipping_migrations
- @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
- @existing_migrations = Dir[@migrations_path + "/*.rb"]
+ assert_equal 2, copied.length
+ assert_equal 0, skipped.length
+ ensure
+ clear
+ end
- sources = ActiveSupport::OrderedHash.new
- sources[:bukkits] = sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
+ def test_copying_migrations_to_non_existing_directory
+ @migrations_path = MIGRATIONS_ROOT + "/non_existing"
+ @existing_migrations = []
- skipped = []
- on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" }
- copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
+ Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
-
- assert_equal 2, skipped.length
- assert_equal ["bukkits PeopleHaveHobbies", "bukkits PeopleHaveDescriptions"], skipped
- ensure
- clear
- end
-
- def test_copying_migrations_to_non_existing_directory
- @migrations_path = MIGRATIONS_ROOT + "/non_existing"
- @existing_migrations = []
-
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb")
- assert_equal 2, copied.length
- end
- ensure
- clear
- Dir.delete(@migrations_path)
end
+ ensure
+ clear
+ Dir.delete(@migrations_path)
+ end
- def test_copying_migrations_to_empty_directory
- @migrations_path = MIGRATIONS_ROOT + "/empty"
- @existing_migrations = []
+ def test_copying_migrations_to_empty_directory
+ @migrations_path = MIGRATIONS_ROOT + "/empty"
+ @existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb")
- assert_equal 2, copied.length
- end
- ensure
- clear
+ Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert_equal 2, copied.length
end
+ ensure
+ clear
end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
new file mode 100644
index 0000000000..1e16addcf3
--- /dev/null
+++ b/activerecord/test/cases/migrator_test.rb
@@ -0,0 +1,377 @@
+require "cases/helper"
+require "cases/migration/helper"
+
+module ActiveRecord
+ class MigratorTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ # Use this class to sense if migrations have gone
+ # up or down.
+ class Sensor < ActiveRecord::Migration
+ attr_reader :went_up, :went_down
+
+ def initialize name = self.class.name, version = nil
+ super
+ @went_up = false
+ @went_down = false
+ end
+
+ def up; @went_up = true; end
+ def down; @went_down = true; end
+ end
+
+ def setup
+ super
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ def teardown
+ super
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ def test_migrator_with_duplicate_names
+ assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ list = [Migration.new('Chunky'), Migration.new('Chunky')]
+ ActiveRecord::Migrator.new(:up, list)
+ end
+ end
+
+ def test_migrator_with_duplicate_versions
+ assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ list = [Migration.new('Foo', 1), Migration.new('Bar', 1)]
+ ActiveRecord::Migrator.new(:up, list)
+ end
+ end
+
+ def test_migrator_with_missing_version_numbers
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [Migration.new('Foo', 1), Migration.new('Bar', 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).run
+ end
+ end
+
+ def test_finds_migrations
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
+ end
+ end
+
+ def test_finds_migrations_in_subdirectories
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
+
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
+ end
+ end
+
+ def test_finds_migrations_from_two_directories
+ directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
+ migrations = ActiveRecord::Migrator.migrations directories
+
+ [[20090101010101, "PeopleHaveHobbies"],
+ [20090101010202, "PeopleHaveDescriptions"],
+ [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
+ [20100201010101, "ValidWithTimestampsWeNeedReminders"],
+ [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
+ assert_equal pair.first, migrations[i].version
+ assert_equal pair.last, migrations[i].name
+ end
+ end
+
+ def test_deprecated_constructor
+ assert_deprecated do
+ ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
+ end
+ end
+
+ def test_relative_migrations
+ list = Dir.chdir(MIGRATIONS_ROOT) do
+ ActiveRecord::Migrator.migrations("valid/")
+ end
+
+ migration_proxy = list.find { |item|
+ item.name == 'ValidPeopleHaveLastNames'
+ }
+ assert migration_proxy, 'should find pending migration'
+ end
+
+ def test_finds_pending_migrations
+ ActiveRecord::SchemaMigration.create!(:version => '1')
+ migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ]
+ migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
+
+ assert_equal 1, migrations.size
+ assert_equal migration_list.last, migrations.first
+ end
+
+ def test_migrator_interleaved_migrations
+ pass_one = [Sensor.new('One', 1)]
+
+ ActiveRecord::Migrator.new(:up, pass_one).migrate
+ assert pass_one.first.went_up
+ refute pass_one.first.went_down
+
+ pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
+ ActiveRecord::Migrator.new(:up, pass_two).migrate
+ refute pass_two[0].went_up
+ assert pass_two[1].went_up
+ assert pass_two.all? { |x| !x.went_down }
+
+ pass_three = [Sensor.new('One', 1),
+ Sensor.new('Two', 2),
+ Sensor.new('Three', 3)]
+
+ ActiveRecord::Migrator.new(:down, pass_three).migrate
+ assert pass_three[0].went_down
+ refute pass_three[1].went_down
+ assert pass_three[2].went_down
+ end
+
+ def test_up_calls_up
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert migrations.all? { |m| m.went_up }
+ assert migrations.all? { |m| !m.went_down }
+ assert_equal 2, ActiveRecord::Migrator.current_version
+ end
+
+ def test_down_calls_down
+ test_up_calls_up
+
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:down, migrations).migrate
+ assert migrations.all? { |m| !m.went_up }
+ assert migrations.all? { |m| m.went_down }
+ assert_equal 0, ActiveRecord::Migrator.current_version
+ end
+
+ def test_current_version
+ ActiveRecord::SchemaMigration.create!(:version => '1000')
+ assert_equal 1000, ActiveRecord::Migrator.current_version
+ end
+
+ def test_migrator_one_up
+ calls, migrations = sensors(3)
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:up, migrations, 2).migrate
+ assert_equal [[:up, 2]], calls
+ end
+
+ def test_migrator_one_down
+ calls, migrations = sensors(3)
+
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:down, migrations, 1).migrate
+
+ assert_equal [[:down, 3], [:down, 2]], calls
+ end
+
+ def test_migrator_one_up_one_down
+ calls, migrations = sensors(3)
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ end
+
+ def test_migrator_double_up
+ calls, migrations = sensors(3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [], calls
+ end
+
+ def test_migrator_double_down
+ calls, migrations = sensors(3)
+
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).run
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [[:down, 1]], calls
+ calls.clear
+
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [], calls
+
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
+
+ def test_migrator_verbosity
+ _, migrations = sensors(3)
+
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
+
+ ActiveRecord::Migration.message_count = 0
+
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
+ ActiveRecord::Migration.message_count = 0
+ end
+
+ def test_migrator_verbosity_off
+ _, migrations = sensors(3)
+
+ ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ end
+
+ def test_target_version_zero_should_run_only_once
+ calls, migrations = sensors(3)
+
+ # migrate up to 1
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ # migrate down to 0
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ calls.clear
+
+ # migrate down to 0 again
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [], calls
+ end
+
+ def test_migrator_going_down_due_to_version_target
+ calls, migrator = migrator_class(3)
+
+ migrator.up("valid", 1)
+ assert_equal [[:up, 1]], calls
+ calls.clear
+
+ migrator.migrate("valid", 0)
+ assert_equal [[:down, 1]], calls
+ calls.clear
+
+ migrator.migrate("valid")
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ end
+
+ def test_migrator_rollback
+ _, migrator = migrator_class(3)
+
+ migrator.migrate("valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+
+ migrator.rollback("valid")
+ assert_equal(2, ActiveRecord::Migrator.current_version)
+
+ migrator.rollback("valid")
+ assert_equal(1, ActiveRecord::Migrator.current_version)
+
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
+
+ def test_migrator_db_has_no_schema_migrations_table
+ _, migrator = migrator_class(3)
+
+ ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
+ refute ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ migrator.migrate("valid", 1)
+ assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ end
+
+ def test_migrator_forward
+ _, migrator = migrator_class(3)
+ migrator.migrate("/valid", 1)
+ assert_equal(1, ActiveRecord::Migrator.current_version)
+
+ migrator.forward("/valid", 2)
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+
+ migrator.forward("/valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+ end
+
+ def test_only_loads_pending_migrations
+ # migrate up to 1
+ ActiveRecord::SchemaMigration.create!(:version => '1')
+
+ calls, migrator = migrator_class(3)
+ migrator.migrate("valid", nil)
+
+ assert_equal [[:up, 2], [:up, 3]], calls
+ end
+
+ def test_get_all_versions
+ _, migrator = migrator_class(3)
+
+ migrator.migrate("valid")
+ assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
+
+ migrator.rollback("valid")
+ assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
+
+ migrator.rollback("valid")
+ assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+
+ migrator.rollback("valid")
+ assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ end
+
+ private
+ def m(name, version, &block)
+ x = Sensor.new name, version
+ x.extend(Module.new {
+ define_method(:up) { block.call(:up, x); super() }
+ define_method(:down) { block.call(:down, x); super() }
+ }) if block_given?
+ end
+
+ def sensors(count)
+ calls = []
+ migrations = count.times.map { |i|
+ m(nil, i + 1) { |c,migration|
+ calls << [c, migration.version]
+ }
+ }
+ [calls, migrations]
+ end
+
+ def migrator_class(count)
+ calls, migrations = sensors(count)
+
+ migrator = Class.new(Migrator).extend(Module.new {
+ define_method(:migrations) { |paths|
+ migrations
+ }
+ })
+ [calls, migrator]
+ end
+ end
+end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index a2041af16a..f7a5d05582 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -72,7 +72,7 @@ class ModulesTest < ActiveRecord::TestCase
clients = []
assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do
- clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL')
+ clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL', :references => :accounts)
clients << MyApplication::Business::Client.find(3, :include => {:firm => :account})
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index bd51388e05..e704322b5d 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -85,7 +85,6 @@ class MultipleDbTest < ActiveRecord::TestCase
end
def test_arel_table_engines
- assert_not_equal Entrant.arel_engine, Course.arel_engine
assert_equal Entrant.arel_engine, Bird.arel_engine
end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 4a09a87322..e17ba76437 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -1,5 +1,4 @@
require "cases/helper"
-require 'active_support/core_ext/array/random_access'
require 'models/post'
require 'models/topic'
require 'models/comment'
@@ -337,6 +336,11 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
+ def test_should_not_duplicates_where_values
+ where_values = Topic.where("1=1").scope_with_lambda.where_values
+ assert_equal ["1=1"], where_values
+ end
+
def test_chaining_with_duplicate_joins
join = "INNER JOIN comments ON comments.post_id = posts.id"
post = Post.find(1)
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index bc3dfb1078..fba3006ebe 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -7,17 +7,17 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def setup
@per_test_teardown = []
- @connection = ActiveRecord::Base.remove_connection
+ @connection = ActiveRecord::Model.remove_connection
end
def teardown
- ActiveRecord::Base.clear_all_connections!
- ActiveRecord::Base.establish_connection(@connection)
+ ActiveRecord::Model.clear_all_connections!
+ ActiveRecord::Model.establish_connection(@connection)
@per_test_teardown.each {|td| td.call }
end
def checkout_connections
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3}))
+ ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3}))
@connections = []
@timed_out = 0
@@ -33,24 +33,16 @@ class PooledConnectionsTest < ActiveRecord::TestCase
end
# Will deadlock due to lack of Monitor timeouts in 1.9
- if RUBY_VERSION < '1.9'
- def test_pooled_connection_checkout
- checkout_connections
- assert_equal 2, @connections.length
- assert_equal 2, @timed_out
- end
- end
-
def checkout_checkin_connections(pool_size, threads)
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5}))
+ ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5}))
@connection_count = 0
@timed_out = 0
threads.times do
Thread.new do
begin
- conn = ActiveRecord::Base.connection_pool.checkout
+ conn = ActiveRecord::Model.connection_pool.checkout
sleep 0.1
- ActiveRecord::Base.connection_pool.checkin conn
+ ActiveRecord::Model.connection_pool.checkin conn
@connection_count += 1
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
@@ -63,13 +55,13 @@ class PooledConnectionsTest < ActiveRecord::TestCase
checkout_checkin_connections 1, 2
assert_equal 2, @connection_count
assert_equal 0, @timed_out
- assert_equal 1, ActiveRecord::Base.connection_pool.connections.size
+ assert_equal 1, ActiveRecord::Model.connection_pool.connections.size
end
private
def add_record(name)
- ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
+ ActiveRecord::Model.connection_pool.with_connection { Project.create! :name => name }
end
end unless current_adapter?(:FrontBase) || in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 4bb5752096..b4d1a631fa 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -23,6 +23,11 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal keyboard.to_key, [keyboard.id]
end
+ def test_read_attribute_with_custom_primary_key
+ keyboard = Keyboard.create!
+ assert_equal keyboard.key_number, keyboard.read_attribute(:id)
+ end
+
def test_to_key_with_primary_key_after_destroy
topic = Topic.find(1)
topic.destroy
@@ -142,8 +147,38 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key
k.primary_key = "foo"
assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key
- k.set_primary_key "bar"
- assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key
+ end
+
+ def test_two_models_with_same_table_but_different_primary_key
+ k1 = Class.new(ActiveRecord::Base)
+ k1.table_name = 'posts'
+ k1.primary_key = 'id'
+
+ k2 = Class.new(ActiveRecord::Base)
+ k2.table_name = 'posts'
+ k2.primary_key = 'title'
+
+ assert k1.columns.find { |c| c.name == 'id' }.primary
+ assert !k1.columns.find { |c| c.name == 'title' }.primary
+ assert k1.columns_hash['id'].primary
+ assert !k1.columns_hash['title'].primary
+
+ assert !k2.columns.find { |c| c.name == 'id' }.primary
+ assert k2.columns.find { |c| c.name == 'title' }.primary
+ assert !k2.columns_hash['id'].primary
+ assert k2.columns_hash['title'].primary
+ end
+
+ def test_models_with_same_table_have_different_columns
+ k1 = Class.new(ActiveRecord::Base)
+ k1.table_name = 'posts'
+
+ k2 = Class.new(ActiveRecord::Base)
+ k2.table_name = 'posts'
+
+ k1.columns.zip(k2.columns).each do |col1, col2|
+ assert !col1.equal?(col2)
+ end
end
end
@@ -153,15 +188,14 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
def test_set_primary_key_with_no_connection
return skip("disconnect wipes in-memory db") if in_memory_db?
- connection = ActiveRecord::Base.remove_connection
+ connection = ActiveRecord::Model.remove_connection
- model = Class.new(ActiveRecord::Base) do
- set_primary_key 'foo'
- end
+ model = Class.new(ActiveRecord::Base)
+ model.primary_key = 'foo'
assert_equal 'foo', model.primary_key
- ActiveRecord::Base.establish_connection(connection)
+ ActiveRecord::Model.establish_connection(connection)
assert_equal 'foo', model.primary_key
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 9554386dcf..f36f4237b1 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -3,7 +3,7 @@ require 'models/topic'
require 'models/task'
require 'models/category'
require 'models/post'
-
+require 'rack'
class QueryCacheTest < ActiveRecord::TestCase
fixtures :tasks, :topics, :categories, :posts, :categories_posts
@@ -43,6 +43,7 @@ class QueryCacheTest < ActiveRecord::TestCase
called = false
mw = ActiveRecord::QueryCache.new lambda { |env|
called = true
+ [200, {}, nil]
}
mw.call({})
assert called, 'middleware should delegate'
@@ -53,6 +54,7 @@ class QueryCacheTest < ActiveRecord::TestCase
Task.find 1
Task.find 1
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
+ [200, {}, nil]
}
mw.call({})
end
@@ -62,6 +64,7 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = ActiveRecord::QueryCache.new lambda { |env|
assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ [200, {}, nil]
}
mw.call({})
end
@@ -83,7 +86,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_cache_off_after_close
- mw = ActiveRecord::QueryCache.new lambda { |env| }
+ mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] }
body = mw.call({}).last
assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled'
@@ -94,6 +97,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_cache_clear_after_close
mw = ActiveRecord::QueryCache.new lambda { |env|
Post.find(:first)
+ [200, {}, nil]
}
body = mw.call({}).last
@@ -244,14 +248,3 @@ 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/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
new file mode 100644
index 0000000000..576ab60090
--- /dev/null
+++ b/activerecord/test/cases/reaper_test.rb
@@ -0,0 +1,81 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ReaperTest < ActiveRecord::TestCase
+ attr_reader :pool
+
+ def setup
+ super
+ @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
+ end
+
+ def teardown
+ super
+ @pool.connections.each(&:close)
+ end
+
+ class FakePool
+ attr_reader :reaped
+
+ def initialize
+ @reaped = false
+ end
+
+ def reap
+ @reaped = true
+ end
+ end
+
+ # A reaper with nil time should never reap connections
+ def test_nil_time
+ fp = FakePool.new
+ assert !fp.reaped
+ reaper = ConnectionPool::Reaper.new(fp, nil)
+ reaper.run
+ assert !fp.reaped
+ end
+
+ def test_some_time
+ fp = FakePool.new
+ assert !fp.reaped
+
+ reaper = ConnectionPool::Reaper.new(fp, 0.0001)
+ reaper.run
+ until fp.reaped
+ Thread.pass
+ end
+ assert fp.reaped
+ end
+
+ def test_pool_has_reaper
+ assert pool.reaper
+ end
+
+ def test_reaping_frequency_configuration
+ spec = ActiveRecord::Base.connection_pool.spec.dup
+ spec.config[:reaping_frequency] = 100
+ pool = ConnectionPool.new spec
+ assert_equal 100, pool.reaper.frequency
+ end
+
+ def test_connection_pool_starts_reaper
+ spec = ActiveRecord::Base.connection_pool.spec.dup
+ spec.config[:reaping_frequency] = 0.0001
+
+ pool = ConnectionPool.new spec
+ pool.timeout = 0
+
+ conn = pool.checkout
+ count = pool.connections.length
+
+ conn.extend(Module.new { def active?; false; end; })
+
+ while count == pool.connections.length
+ Thread.pass
+ end
+ assert_equal(count - 1, pool.connections.length)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index b30db542a7..297fb56570 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -36,25 +36,25 @@ class ReflectionTest < ActiveRecord::TestCase
def test_read_attribute_names
assert_equal(
- %w( id title author_name author_email_address bonus_time written_on last_read content group approved replies_count parent_id parent_title type created_at updated_at ).sort,
+ %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count parent_id parent_title type created_at updated_at ).sort,
@first.attribute_names.sort
)
end
def test_columns
- assert_equal 16, Topic.columns.length
+ assert_equal 17, Topic.columns.length
end
def test_columns_are_returned_in_the_order_they_were_declared
column_names = Topic.columns.map { |column| column.name }
- assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type group created_at updated_at), column_names
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count parent_id parent_title type group created_at updated_at), column_names
end
def test_content_columns
content_columns = Topic.content_columns
content_column_names = content_columns.map {|column| column.name}
- assert_equal 12, content_columns.length
- assert_equal %w(title author_name author_email_address written_on bonus_time last_read content group approved parent_title created_at updated_at).sort, content_column_names.sort
+ assert_equal 13, content_columns.length
+ assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort
end
def test_column_string_type_and_limit
@@ -189,8 +189,8 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
- assert_equal 37, Firm.reflect_on_all_associations.size
- assert_equal 27, Firm.reflect_on_all_associations(:has_many).size
+ assert_equal 38, Firm.reflect_on_all_associations.size
+ assert_equal 28, Firm.reflect_on_all_associations(:has_many).size
assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb
index 1e2093273e..edf38cb7a3 100644
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ b/activerecord/test/cases/relation_scoping_test.rb
@@ -106,7 +106,7 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_scoped_find_include
# with the include, will retrieve only developers for the given project
scoped_developers = Developer.includes(:projects).scoping do
- Developer.where('projects.id = 2').all
+ Developer.where('projects.id' => 2).all
end
assert scoped_developers.include?(developers(:david))
assert !scoped_developers.include?(developers(:jamis))
@@ -421,6 +421,12 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal expected, received
end
+ def test_order_after_reorder_combines_orders
+ expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
+ received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+ end
+
def test_nested_exclusive_scope
expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 715a378431..ac6dee3c6a 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, :from, :reorder, :reverse_order, :uniq].map(&:to_s).sort,
+ assert_equal [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq].map(&:to_s).sort,
Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort
end
@@ -44,7 +44,7 @@ module ActiveRecord
end
def test_multi_value_methods
- assert_equal [:select, :group, :order, :joins, :where, :having, :bind].map(&:to_s).sort,
+ assert_equal [:select, :group, :order, :joins, :where, :having, :bind, :references].map(&:to_s).sort,
Relation::MULTI_VALUE_METHODS.map(&:to_s).sort
end
@@ -135,5 +135,24 @@ module ActiveRecord
relation.eager_load_values << :b
assert relation.eager_loading?
end
+
+ def test_references_values
+ relation = Relation.new :a, :b
+ assert_equal [], relation.references_values
+ relation = relation.references(:foo).references(:omg, :lol)
+ assert_equal ['foo', 'omg', 'lol'], relation.references_values
+ end
+
+ def test_references_values_dont_duplicate
+ relation = Relation.new :a, :b
+ relation = relation.references(:foo).references(:foo)
+ assert_equal ['foo'], relation.references_values
+ end
+
+ def test_apply_finder_options_takes_references
+ relation = Relation.new :a, :b
+ relation = relation.apply_finder_options(:references => :foo)
+ assert_equal ['foo'], relation.references_values
+ end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index bf1eb6386a..5e19465253 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -184,12 +184,12 @@ class RelationTest < ActiveRecord::TestCase
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
+ tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order
- tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
+ tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
assert_equal 3, tags.length
end
@@ -1104,7 +1104,9 @@ class RelationTest < ActiveRecord::TestCase
)
)
- assert scope.eager_loading?
+ assert_deprecated do
+ assert scope.eager_loading?
+ end
end
def test_ordering_with_extra_spaces
@@ -1164,4 +1166,49 @@ class RelationTest < ActiveRecord::TestCase
end
assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
end
+
+ def test_references_triggers_eager_loading
+ scope = Post.includes(:comments)
+ assert !scope.eager_loading?
+ assert scope.references(:comments).eager_loading?
+ end
+
+ def test_references_doesnt_trigger_eager_loading_if_reference_not_included
+ scope = Post.references(:comments)
+ assert !scope.eager_loading?
+ end
+
+ def test_automatically_added_where_references
+ scope = Post.where(:comments => { :body => "Bla" })
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.where('comments.body' => 'Bla')
+ assert_equal ['comments'], scope.references_values
+ end
+
+ def test_automatically_added_having_references
+ scope = Post.having(:comments => { :body => "Bla" })
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.having('comments.body' => 'Bla')
+ assert_equal ['comments'], scope.references_values
+ end
+
+ def test_automatically_added_order_references
+ scope = Post.order('comments.body')
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.order('comments.body', 'yaks.body')
+ assert_equal ['comments', 'yaks'], scope.references_values
+
+ # Don't infer yaks, let's not go down that road again...
+ scope = Post.order('comments.body, yaks.body')
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.order('comments.body asc')
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.order('foo(comments.body)')
+ assert_equal [], scope.references_values
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 5c3a78688e..abeb56fd3f 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -2,7 +2,13 @@ require "cases/helper"
class SchemaDumperTest < ActiveRecord::TestCase
+ def initialize(*)
+ super
+ ActiveRecord::SchemaMigration.create_table
+ end
+
def setup
+ super
@stream = StringIO.new
end
@@ -13,10 +19,18 @@ class SchemaDumperTest < ActiveRecord::TestCase
@stream.string
end
- if "string".encoding_aware?
- def test_magic_comment
- assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
+ def test_dump_schema_information_outputs_lexically_ordered_versions
+ versions = %w{ 20100101010101 20100201010101 20100301010101 }
+ versions.reverse.each do |v|
+ ActiveRecord::SchemaMigration.create!(:version => v)
end
+
+ schema_info = ActiveRecord::Base.connection.dump_schema_information
+ assert_match(/20100201010101.*20100301010101/m, schema_info)
+ end
+
+ def test_magic_comment
+ assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
end
def test_schema_dump
diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb
index 258cee7aba..a3b8ab74d9 100644
--- a/activerecord/test/cases/session_store/session_test.rb
+++ b/activerecord/test/cases/session_store/session_test.rb
@@ -5,11 +5,15 @@ require 'active_record/session_store'
module ActiveRecord
class SessionStore
class SessionTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints? && ActiveRecord::Base.connection.supports_ddl_transactions?
+ self.use_transactional_fixtures = false
+
+ attr_reader :session_klass
def setup
super
+ ActiveRecord::Base.connection.schema_cache.clear!
Session.drop_table! if Session.table_exists?
+ @session_klass = Class.new(Session)
end
def test_data_column_name
@@ -60,8 +64,8 @@ module ActiveRecord
def test_find_by_session_id
Session.create_table!
session_id = "10"
- s = Session.create!(:data => 'world', :session_id => session_id)
- t = Session.find_by_session_id(session_id)
+ s = session_klass.create!(:data => 'world', :session_id => session_id)
+ t = session_klass.find_by_session_id(session_id)
assert_equal s, t
assert_equal s.data, t.data
Session.drop_table!
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 074fd39e65..5a3f9a9711 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -24,9 +24,9 @@ class StoreTest < ActiveRecord::TestCase
@john.settings[:icecream] = 'graeters'
@john.save
- assert 'graeters', @john.reload.settings[:icecream]
+ assert_equal 'graeters', @john.reload.settings[:icecream]
end
-
+
test "updating the store will mark it as changed" do
@john.color = 'red'
assert @john.settings_changed?
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 85f222bca2..f8b3e01a49 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -6,7 +6,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
fixtures :topics
class TopicWithCallbacks < ActiveRecord::Base
- set_table_name :topics
+ self.table_name = :topics
after_commit{|record| record.send(:do_after_commit, nil)}
after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
@@ -252,7 +252,7 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase
fixtures :topics
class TopicWithObserverAttached < ActiveRecord::Base
- set_table_name :topics
+ self.table_name = :topics
def history
@history ||= []
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 110a18772f..203dd054f1 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -563,6 +563,7 @@ if current_adapter?(:PostgreSQLAdapter)
topic.approved = !topic.approved?
topic.save!
end
+ Topic.connection.close
end
end
@@ -598,6 +599,7 @@ if current_adapter?(:PostgreSQLAdapter)
dev = Developer.find(1)
assert_equal original_salary, dev.salary
end
+ Developer.connection.close
end
end
@@ -610,6 +612,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal original_salary, Developer.find(1).salary
end
end
+ Developer.connection.close
end
threads.each { |t| t.join }
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index e82ca3f93d..5a69054445 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -7,13 +7,13 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
- @underlying = ActiveRecord::Base.connection
- @specification = ActiveRecord::Base.remove_connection
+ @underlying = ActiveRecord::Model.connection
+ @specification = ActiveRecord::Model.remove_connection
end
def teardown
@underlying = nil
- ActiveRecord::Base.establish_connection(@specification)
+ ActiveRecord::Model.establish_connection(@specification)
load_schema if in_memory_db?
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 56e345990f..c72b7b35cd 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -61,6 +61,16 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert r.valid?
end
+ def test_validates_associated_marked_for_destruction
+ Topic.validates_associated(:replies)
+ Reply.validates_presence_of(:content)
+ t = Topic.new
+ t.replies << Reply.new
+ assert t.invalid?
+ t.replies.first.mark_for_destruction
+ assert t.valid?
+ end
+
def test_validates_associated_with_custom_message_using_quotes
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
@@ -81,14 +91,12 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
def test_validates_size_of_association_utf8
- with_kcode('UTF8') do
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'あいうえおかきくけこ')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'あいうえおかきくけこ')
- assert o.valid?
- end
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'あいうえおかきくけこ')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'あいうえおかきくけこ')
+ assert o.valid?
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
@@ -110,4 +118,21 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
end
+ def test_validates_associated_models_in_the_same_context
+ Topic.validates_presence_of :title, :on => :custom_context
+ Topic.validates_associated :replies
+ Reply.validates_presence_of :title, :on => :custom_context
+
+ t = Topic.new('title' => '')
+ r = t.replies.new('title' => '')
+
+ assert t.valid?
+ assert !t.valid?(:custom_context)
+
+ t.title = "Longer"
+ assert !t.valid?(:custom_context), "Should NOT be valid if the associated object is not valid in the same context."
+
+ r.title = "Longer"
+ assert t.valid?(:custom_context), "Should be valid if the associated object is not valid in the same context."
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 0f1b3667cc..79442d68b0 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -45,6 +45,18 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.save, "Should now save t2 as unique"
end
+ def test_validates_uniqueness_with_nil_value
+ Topic.validates_uniqueness_of(:title)
+
+ t = Topic.new("title" => nil)
+ assert t.save, "Should save t as unique"
+
+ t2 = Topic.new("title" => nil)
+ assert !t2.valid?, "Shouldn't be valid"
+ assert !t2.save, "Shouldn't save t2 as unique"
+ assert_equal ["has already been taken"], t2.errors[:title]
+ end
+
def test_validates_uniqueness_with_validates
Topic.validates :title, :uniqueness => true
Topic.create!('title' => 'abc')
@@ -80,6 +92,30 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r3.valid?, "Saving r3"
end
+ def test_validate_uniqueness_with_object_scope
+ Reply.validates_uniqueness_of(:content, :scope => :topic)
+
+ t = Topic.create("title" => "I'm unique!")
+
+ r1 = t.replies.create "title" => "r1", "content" => "hello world"
+ assert r1.valid?, "Saving r1"
+
+ r2 = t.replies.create "title" => "r2", "content" => "hello world"
+ assert !r2.valid?, "Saving r2 first time"
+ end
+
+ def test_validate_uniqueness_with_object_arg
+ Reply.validates_uniqueness_of(:topic)
+
+ t = Topic.create("title" => "I'm unique!")
+
+ r1 = t.replies.create "title" => "r1", "content" => "hello world"
+ assert r1.valid?, "Saving r1"
+
+ r2 = t.replies.create "title" => "r2", "content" => "hello world"
+ assert !r2.valid?, "Saving r2 first time"
+ end
+
def test_validate_uniqueness_scoped_to_defining_class
t = Topic.create("title" => "What, me worry?")
@@ -149,16 +185,14 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.valid?, "should validate with nil"
assert t2.save, "should save with nil"
- with_kcode('UTF8') do
- t_utf8 = Topic.new("title" => "Я тоже уникальный!")
- assert t_utf8.save, "Should save t_utf8 as unique"
+ t_utf8 = Topic.new("title" => "Я тоже уникальный!")
+ assert t_utf8.save, "Should save t_utf8 as unique"
- # If database hasn't UTF-8 character set, this test fails
- if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!"
- t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
- assert !t2_utf8.valid?, "Shouldn't be valid"
- assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
- end
+ # If database hasn't UTF-8 character set, this test fails
+ if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!"
+ t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
+ assert !t2_utf8.valid?, "Shouldn't be valid"
+ assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
end
end
@@ -256,13 +290,11 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_limit_and_utf8
- with_kcode('UTF8') do
- # Event.title is limited to 5 characters
- e1 = Event.create(:title => "一二三四五")
- assert e1.valid?, "Could not create an event with a unique, 5 character title"
- e2 = Event.create(:title => "一二三四五六七八")
- assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
- end
+ # Event.title is limited to 5 characters
+ e1 = Event.create(:title => "一二三四五")
+ assert e1.valid?, "Could not create an event with a unique, 5 character title"
+ e2 = Event.create(:title => "一二三四五六七八")
+ assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
end
def test_validate_straight_inheritance_uniqueness
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index c3e494866b..e575a98170 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -8,7 +8,7 @@ require 'models/parrot'
require 'models/company'
class ProtectedPerson < ActiveRecord::Base
- set_table_name 'people'
+ self.table_name = 'people'
attr_accessor :addon
attr_protected :first_name
end
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 0b54c309d1..302913e095 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,14 +1,20 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/topic'
class YamlSerializationTest < ActiveRecord::TestCase
fixtures :topics
def test_to_yaml_with_time_with_zone_should_not_raise_exception
+ tz = Time.zone
Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
ActiveRecord::Base.time_zone_aware_attributes = true
+
topic = Topic.new(:written_on => DateTime.now)
assert_nothing_raised { topic.to_yaml }
+
+ ensure
+ Time.zone = tz
+ ActiveRecord::Base.time_zone_aware_attributes = false
end
def test_roundtrip
@@ -18,6 +24,11 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal topic, t
end
+ def test_roundtrip_serialized_column
+ topic = Topic.new(:content => {:omg=>:lol})
+ assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content)
+ end
+
def test_encode_with_coder
topic = Topic.first
coder = {}
@@ -25,22 +36,17 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal({'attributes' => topic.attributes}, coder)
end
- begin
- require 'psych'
-
- def test_psych_roundtrip
- topic = Topic.first
- assert topic
- t = Psych.load Psych.dump topic
- assert_equal topic, t
- end
-
- def test_psych_roundtrip_new_object
- topic = Topic.new
- assert topic
- t = Psych.load Psych.dump topic
- assert_equal topic.attributes, t.attributes
- end
- rescue LoadError
+ def test_psych_roundtrip
+ topic = Topic.first
+ assert topic
+ t = Psych.load Psych.dump topic
+ assert_equal topic, t
+ end
+
+ def test_psych_roundtrip_new_object
+ topic = Topic.new
+ assert topic
+ t = Psych.load Psych.dump topic
+ assert_equal topic.attributes, t.attributes
end
end
diff --git a/activerecord/test/fixtures/admin/randomly_named_a9.yml b/activerecord/test/fixtures/admin/randomly_named_a9.yml
new file mode 100644
index 0000000000..bc51c83112
--- /dev/null
+++ b/activerecord/test/fixtures/admin/randomly_named_a9.yml
@@ -0,0 +1,7 @@
+first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/admin/randomly_named_b0.yml b/activerecord/test/fixtures/admin/randomly_named_b0.yml
new file mode 100644
index 0000000000..bc51c83112
--- /dev/null
+++ b/activerecord/test/fixtures/admin/randomly_named_b0.yml
@@ -0,0 +1,7 @@
+first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml
new file mode 100644
index 0000000000..d3e5e4a1aa
--- /dev/null
+++ b/activerecord/test/fixtures/dog_lovers.yml
@@ -0,0 +1,4 @@
+david:
+ id: 1
+ bred_dogs_count: 0
+ trained_dogs_count: 1
diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml
new file mode 100644
index 0000000000..16d19be2c5
--- /dev/null
+++ b/activerecord/test/fixtures/dogs.yml
@@ -0,0 +1,3 @@
+sophie:
+ id: 1
+ trainer_id: 1
diff --git a/activerecord/test/fixtures/other_topics.yml b/activerecord/test/fixtures/other_topics.yml
new file mode 100644
index 0000000000..93f48aedc4
--- /dev/null
+++ b/activerecord/test/fixtures/other_topics.yml
@@ -0,0 +1,42 @@
+first:
+ id: 1
+ title: The First Topic
+ author_name: David
+ author_email_address: david@loudthinking.com
+ written_on: 2003-07-16t15:28:11.2233+01:00
+ last_read: 2004-04-15
+ bonus_time: 2005-01-30t15:28:00.00+01:00
+ content: Have a nice day
+ approved: false
+ replies_count: 1
+
+second:
+ id: 2
+ title: The Second Topic of the day
+ author_name: Mary
+ written_on: 2004-07-15t15:28:00.0099+01:00
+ content: Have a nice day
+ approved: true
+ replies_count: 0
+ parent_id: 1
+ type: Reply
+
+third:
+ id: 3
+ title: The Third Topic of the day
+ author_name: Carl
+ written_on: 2005-07-15t15:28:00.0099+01:00
+ content: I'm a troll
+ approved: true
+ replies_count: 1
+
+fourth:
+ id: 4
+ title: The Fourth Topic of the day
+ author_name: Carl
+ written_on: 2006-07-15t15:28:00.0099+01:00
+ content: Why not?
+ approved: true
+ type: Reply
+ parent_id: 3
+
diff --git a/activerecord/test/fixtures/randomly_named_a9.yml b/activerecord/test/fixtures/randomly_named_a9.yml
new file mode 100644
index 0000000000..bc51c83112
--- /dev/null
+++ b/activerecord/test/fixtures/randomly_named_a9.yml
@@ -0,0 +1,7 @@
+first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml
new file mode 100644
index 0000000000..ff515beb45
--- /dev/null
+++ b/activerecord/test/fixtures/teapots.yml
@@ -0,0 +1,3 @@
+bob:
+ id: 1
+ name: Bob
diff --git a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
deleted file mode 100644
index ffb224dad9..0000000000
--- a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class MigrationThatRaisesException < ActiveRecord::Migration
- def self.up
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
-
- def self.down
- remove_column "people", "last_name"
- end
-end
diff --git a/activerecord/test/migrations/duplicate/3_foo.rb b/activerecord/test/migrations/duplicate/3_foo.rb
deleted file mode 100644
index 916fe580fa..0000000000
--- a/activerecord/test/migrations/duplicate/3_foo.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Foo < ActiveRecord::Migration
- def self.up
- end
-
- def self.down
- end
-end \ No newline at end of file
diff --git a/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb b/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb
deleted file mode 100644
index 5fe5089e18..0000000000
--- a/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Chunky < ActiveRecord::Migration
- def self.up
- end
-
- def self.down
- end
-end
diff --git a/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb b/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb
deleted file mode 100644
index 5fe5089e18..0000000000
--- a/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Chunky < ActiveRecord::Migration
- def self.up
- end
-
- def self.down
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb
deleted file mode 100644
index bf912fbfc8..0000000000
--- a/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class InterleavedInnocentJointable < ActiveRecord::Migration
- def self.up
- create_table("people_reminders", :id => false) do |t|
- t.column :reminder_id, :integer
- t.column :person_id, :integer
- end
- end
-
- def self.down
- drop_table "people_reminders"
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb
deleted file mode 100644
index c6c94213a0..0000000000
--- a/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class InterleavedPeopleHaveLastNames < ActiveRecord::Migration
- def self.up
- add_column "people", "last_name", :string
- end
-
- def self.down
- remove_column "people", "last_name"
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb
deleted file mode 100644
index bf912fbfc8..0000000000
--- a/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class InterleavedInnocentJointable < ActiveRecord::Migration
- def self.up
- create_table("people_reminders", :id => false) do |t|
- t.column :reminder_id, :integer
- t.column :person_id, :integer
- end
- end
-
- def self.down
- drop_table "people_reminders"
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb
deleted file mode 100644
index c6c94213a0..0000000000
--- a/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class InterleavedPeopleHaveLastNames < ActiveRecord::Migration
- def self.up
- add_column "people", "last_name", :string
- end
-
- def self.down
- remove_column "people", "last_name"
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb b/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb
deleted file mode 100644
index 6849995f5e..0000000000
--- a/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class InterleavedIRaiseOnDown < ActiveRecord::Migration
- def self.up
- end
-
- def self.down
- raise
- end
-end
diff --git a/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb
deleted file mode 100644
index bf912fbfc8..0000000000
--- a/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class InterleavedInnocentJointable < ActiveRecord::Migration
- def self.up
- create_table("people_reminders", :id => false) do |t|
- t.column :reminder_id, :integer
- t.column :person_id, :integer
- end
- end
-
- def self.down
- drop_table "people_reminders"
- end
-end
diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb
new file mode 100644
index 0000000000..cdbe0b1679
--- /dev/null
+++ b/activerecord/test/migrations/rename/1_we_need_things.rb
@@ -0,0 +1,11 @@
+class WeNeedThings < ActiveRecord::Migration
+ def self.up
+ create_table("things") do |t|
+ t.column :content, :text
+ end
+ end
+
+ def self.down
+ drop_table "things"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb
new file mode 100644
index 0000000000..d441b71fc9
--- /dev/null
+++ b/activerecord/test/migrations/rename/2_rename_things.rb
@@ -0,0 +1,9 @@
+class RenameThings < ActiveRecord::Migration
+ def self.up
+ rename_table "things", "awesome_things"
+ end
+
+ def self.down
+ rename_table "awesome_things", "things"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb
new file mode 100644
index 0000000000..e438cf5999
--- /dev/null
+++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb
@@ -0,0 +1,9 @@
+class PeopleHaveLastNames < ActiveRecord::Migration
+ def self.up
+ add_column "people", "hobbies", :string
+ end
+
+ def self.down
+ remove_column "people", "hobbies"
+ end
+end
diff --git a/activerecord/test/migrations/duplicate/1_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb
index 81af5fef5e..06cb911117 100644
--- a/activerecord/test/migrations/duplicate/1_people_have_last_names.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb
@@ -1,4 +1,4 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+class ValidPeopleHaveLastNames < ActiveRecord::Migration
def self.up
add_column "people", "last_name", :string
end
@@ -6,4 +6,4 @@ class PeopleHaveLastNames < ActiveRecord::Migration
def self.down
remove_column "people", "last_name"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/duplicate/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
index d5e71ce8ef..d5e71ce8ef 100644
--- a/activerecord/test/migrations/duplicate/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
diff --git a/activerecord/test/migrations/duplicate/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
index 21c9ca5328..21c9ca5328 100644
--- a/activerecord/test/migrations/duplicate/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb
new file mode 100644
index 0000000000..2f81d5b831
--- /dev/null
+++ b/activerecord/test/models/admin/randomly_named_c1.rb
@@ -0,0 +1,3 @@
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
+ self.table_name = :randomly_named_table
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 23db5650d4..d50e11d6c9 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -49,12 +49,12 @@ class Author < ActiveRecord::Base
has_many :sti_post_comments, :through => :sti_posts, :source => :comments
has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'"
- has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => "comments.post_id = 0"
+ has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => { 'comments.post_id' => 0 }
has_many :nonexistant_comments, :through => :posts
has_many :hello_posts, :class_name => "Post", :conditions => "posts.body = 'hello'"
has_many :hello_post_comments, :through => :hello_posts, :source => :comments
- has_many :posts_with_no_comments, :class_name => 'Post', :conditions => 'comments.id is null', :include => :comments
+ has_many :posts_with_no_comments, :class_name => 'Post', :conditions => { 'comments.id' => nil }, :include => :comments
has_many :hello_posts_with_hash_conditions, :class_name => "Post",
:conditions => {:body => 'hello'}
@@ -128,7 +128,6 @@ class Author < ActiveRecord::Base
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
- has_many :post_categories, :through => :posts, :source => :categories
has_many :category_post_comments, :through => :categories, :source => :post_comments
has_many :misc_posts, :class_name => 'Post',
diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb
index e61d48e6a5..dff099c1fb 100644
--- a/activerecord/test/models/bird.rb
+++ b/activerecord/test/models/bird.rb
@@ -1,9 +1,12 @@
class Bird < ActiveRecord::Base
+ belongs_to :pirate
validates_presence_of :name
+ accepts_nested_attributes_for :pirate
+
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
false
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index b9c2e8ec9a..6ff1329d8e 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -9,7 +9,7 @@ class Car < ActiveRecord::Base
has_many :tyres
has_many :engines, :dependent => :destroy
- has_many :wheels, :as => :wheelable
+ has_many :wheels, :as => :wheelable, :dependent => :destroy
scope :incl_tyres, includes(:tyres)
scope :incl_engines, includes(:engines)
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 78eb4c57ac..d1a8a82786 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -4,7 +4,7 @@ end
class Company < AbstractCompany
attr_protected :rating
- set_sequence_name :companies_nonstd_seq
+ self.sequence_name = :companies_nonstd_seq
validates_presence_of :name
@@ -88,6 +88,8 @@ class Firm < Company
has_many :accounts
has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
+ has_many :association_with_references, :class_name => 'Client', :references => :foo
+
def log
@log ||= []
end
diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb
index 15e3a1de0b..7db9a4e731 100644
--- a/activerecord/test/models/country.rb
+++ b/activerecord/test/models/country.rb
@@ -1,6 +1,6 @@
class Country < ActiveRecord::Base
- set_primary_key :country_id
+ self.primary_key = :country_id
has_and_belongs_to_many :treaties
diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb
index a8a25834b1..1b3b54545f 100644
--- a/activerecord/test/models/dashboard.rb
+++ b/activerecord/test/models/dashboard.rb
@@ -1,3 +1,3 @@
class Dashboard < ActiveRecord::Base
- set_primary_key :dashboard_id
-end \ No newline at end of file
+ self.primary_key = :dashboard_id
+end
diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb
new file mode 100644
index 0000000000..72b7d33a86
--- /dev/null
+++ b/activerecord/test/models/dog.rb
@@ -0,0 +1,4 @@
+class Dog < ActiveRecord::Base
+ belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
+ belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
+end
diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb
new file mode 100644
index 0000000000..a33dc575c5
--- /dev/null
+++ b/activerecord/test/models/dog_lover.rb
@@ -0,0 +1,4 @@
+class DogLover < ActiveRecord::Base
+ has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id
+ has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id
+end
diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb
index d7f01e59e6..edda4655dc 100644
--- a/activerecord/test/models/joke.rb
+++ b/activerecord/test/models/joke.rb
@@ -1,7 +1,7 @@
class Joke < ActiveRecord::Base
- set_table_name 'funny_jokes'
+ self.table_name = 'funny_jokes'
end
class GoodJoke < ActiveRecord::Base
- set_table_name 'funny_jokes'
+ self.table_name = 'funny_jokes'
end
diff --git a/activerecord/test/models/keyboard.rb b/activerecord/test/models/keyboard.rb
index 32a4a7fad0..39347e274e 100644
--- a/activerecord/test/models/keyboard.rb
+++ b/activerecord/test/models/keyboard.rb
@@ -1,3 +1,3 @@
class Keyboard < ActiveRecord::Base
- set_primary_key 'key_number'
+ self.primary_key = 'key_number'
end
diff --git a/activerecord/test/models/legacy_thing.rb b/activerecord/test/models/legacy_thing.rb
index eaeb642d12..eead181a0e 100644
--- a/activerecord/test/models/legacy_thing.rb
+++ b/activerecord/test/models/legacy_thing.rb
@@ -1,3 +1,3 @@
class LegacyThing < ActiveRecord::Base
- set_locking_column :version
+ self.locking_column = :version
end
diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb
index b96c054f6c..3fcd5e4b69 100644
--- a/activerecord/test/models/liquid.rb
+++ b/activerecord/test/models/liquid.rb
@@ -1,5 +1,5 @@
class Liquid < ActiveRecord::Base
- set_table_name :liquid
+ self.table_name = :liquid
has_many :molecules, :uniq => true
end
diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb
index 830cdb5796..4fe79720ad 100644
--- a/activerecord/test/models/minivan.rb
+++ b/activerecord/test/models/minivan.rb
@@ -1,5 +1,5 @@
class Minivan < ActiveRecord::Base
- set_primary_key :minivan_id
+ self.primary_key = :minivan_id
belongs_to :speedometer
has_one :dashboard, :through => :speedometer
diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb
index 853f2682b3..763baefd91 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,3 @@
class MixedCaseMonkey < ActiveRecord::Base
- set_primary_key 'monkeyID'
+ self.primary_key = 'monkeyID'
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index 5760b991ec..fea55f4535 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,5 +1,5 @@
class Owner < ActiveRecord::Base
- set_primary_key :owner_id
+ self.primary_key = :owner_id
has_many :pets
has_many :toys, :through => :pets
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index 737ef9131b..c4ee2bd19d 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -1,5 +1,6 @@
class Parrot < ActiveRecord::Base
- set_inheritance_column :parrot_sti_class
+ self.inheritance_column = :parrot_sti_class
+
has_and_belongs_to_many :pirates
has_and_belongs_to_many :treasures
has_many :loots, :as => :looter
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index 967a3625aa..d2a0c6b40c 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -3,7 +3,8 @@ class Person < ActiveRecord::Base
has_one :reader
has_many :posts, :through => :readers
- has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, :conditions => 'comments.id is null'
+ has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments,
+ :conditions => 'comments.id is null', :references => :comments
has_many :references
has_many :bad_references
@@ -54,7 +55,7 @@ class LoosePerson < ActiveRecord::Base
self.table_name = 'people'
self.abstract_class = true
- attr_protected :comments
+ attr_protected :comments, :best_friend_id, :best_friend_of_id
attr_protected :as => :admin
has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
@@ -81,4 +82,4 @@ class TightPerson < ActiveRecord::Base
accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
end
-class TightDescendant < TightPerson; end \ No newline at end of file
+class TightDescendant < TightPerson; end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index 113826756a..3cd5bceed5 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -2,7 +2,7 @@ class Pet < ActiveRecord::Base
attr_accessor :current_user
- set_primary_key :pet_id
+ self.primary_key = :pet_id
belongs_to :owner, :touch => true
has_many :toys
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 198a963cbc..1cab78d8c7 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -24,6 +24,10 @@ class Post < ActiveRecord::Base
belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts
belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address
+ def first_comment
+ super.body
+ end
+ has_one :first_comment, :class_name => 'Comment', :order => 'id ASC'
has_one :last_comment, :class_name => 'Comment', :order => 'id desc'
scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} }
@@ -40,6 +44,10 @@ class Post < ActiveRecord::Base
def newest
created.last
end
+
+ def the_association
+ proxy_association
+ end
end
has_many :author_favorites, :through => :author
@@ -181,4 +189,4 @@ end
class SpecialPostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope where(:id => [1, 5,6])
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb
new file mode 100644
index 0000000000..18a86c4989
--- /dev/null
+++ b/activerecord/test/models/randomly_named_c1.rb
@@ -0,0 +1,3 @@
+class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
+ self.table_name = :randomly_named_table
+end
diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb
index 94743eff8e..0a7d38d8ec 100644
--- a/activerecord/test/models/speedometer.rb
+++ b/activerecord/test/models/speedometer.rb
@@ -1,4 +1,4 @@
class Speedometer < ActiveRecord::Base
- set_primary_key :speedometer_id
+ self.primary_key = :speedometer_id
belongs_to :dashboard
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb
index f8d4c6e0e4..f084ec1bdc 100644
--- a/activerecord/test/models/string_key_object.rb
+++ b/activerecord/test/models/string_key_object.rb
@@ -1,3 +1,3 @@
class StringKeyObject < ActiveRecord::Base
- set_primary_key :id
+ self.primary_key = :id
end
diff --git a/activerecord/test/models/subscriber.rb b/activerecord/test/models/subscriber.rb
index 5b78014e6f..76e85a0cd3 100644
--- a/activerecord/test/models/subscriber.rb
+++ b/activerecord/test/models/subscriber.rb
@@ -1,5 +1,5 @@
class Subscriber < ActiveRecord::Base
- set_primary_key 'nick'
+ self.primary_key = 'nick'
has_many :subscriptions
has_many :books, :through => :subscriptions
end
diff --git a/activerecord/test/models/teapot.rb b/activerecord/test/models/teapot.rb
new file mode 100644
index 0000000000..b035b18c1b
--- /dev/null
+++ b/activerecord/test/models/teapot.rb
@@ -0,0 +1,35 @@
+class Teapot
+ # I'm a little teapot,
+ # Short and stout,
+ # Here is my handle
+ # Here is my spout
+ # When I get all steamed up,
+ # Hear me shout,
+ # Tip me over and pour me out!
+ #
+ # HELL YEAH TEAPOT SONG
+
+ include ActiveRecord::Model
+end
+
+class OtherTeapot < Teapot
+end
+
+class OMFGIMATEAPOT
+ def aaahhh
+ "mmm"
+ end
+end
+
+class CoolTeapot < OMFGIMATEAPOT
+ include ActiveRecord::Model
+ self.table_name = "teapots"
+end
+
+class Ceiling
+ include ActiveRecord::Model
+
+ class Teapot
+ include ActiveRecord::Model
+ end
+end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index fe424e61b2..8bcb9df8a8 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -8,6 +8,8 @@ class Topic < ActiveRecord::Base
scope :approved, :conditions => {:approved => true}
scope :rejected, :conditions => {:approved => false}
+ scope :scope_with_lambda, lambda { scoped }
+
scope :by_lifo, :conditions => {:author_name => 'lifo'}
scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}}
@@ -78,6 +80,11 @@ class Topic < ActiveRecord::Base
after_initialize :set_email_address
+ class_attribute :after_initialize_called
+ after_initialize do
+ self.class.after_initialize_called = true
+ end
+
def approved=(val)
@custom_approved = val
write_attribute(:approved, val)
@@ -106,6 +113,10 @@ class Topic < ActiveRecord::Base
def after_create_for_transaction; end
end
+class ImportantTopic < Topic
+ serialize :important, Hash
+end
+
module Web
class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb
index 6c45e99671..0377e50011 100644
--- a/activerecord/test/models/toy.rb
+++ b/activerecord/test/models/toy.rb
@@ -1,5 +1,5 @@
class Toy < ActiveRecord::Base
- set_primary_key :toy_id
+ self.primary_key = :toy_id
belongs_to :pet
scope :with_pet, joins(:pet)
diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb
index b46537f0d2..41fd1350f3 100644
--- a/activerecord/test/models/treaty.rb
+++ b/activerecord/test/models/treaty.rb
@@ -1,6 +1,6 @@
class Treaty < ActiveRecord::Base
- set_primary_key :treaty_id
+ self.primary_key = :treaty_id
has_and_belongs_to_many :countries
diff --git a/activerecord/test/models/warehouse_thing.rb b/activerecord/test/models/warehouse_thing.rb
index 6ace1183cc..f20bd1a245 100644
--- a/activerecord/test/models/warehouse_thing.rb
+++ b/activerecord/test/models/warehouse_thing.rb
@@ -1,5 +1,5 @@
class WarehouseThing < ActiveRecord::Base
- set_table_name "warehouse-things"
+ self.table_name = "warehouse-things"
validates_uniqueness_of :value
-end \ No newline at end of file
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index bb08f5c181..e2cd7ce9e4 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -107,6 +107,7 @@ ActiveRecord::Schema.define do
t.string :name
t.integer :engines_count
t.integer :wheels_count
+ t.column :lock_version, :integer, :null => false, :default => 0
end
create_table :categories, :force => true do |t|
@@ -212,6 +213,16 @@ ActiveRecord::Schema.define do
t.integer :access_level, :default => 1
end
+ create_table :dog_lovers, :force => true do |t|
+ t.integer :trained_dogs_count, :default => 0
+ t.integer :bred_dogs_count, :default => 0
+ end
+
+ create_table :dogs, :force => true do |t|
+ t.integer :trainer_id
+ t.integer :breeder_id
+ end
+
create_table :edges, :force => true, :id => false do |t|
t.column :source_id, :integer, :null => false
t.column :sink_id, :integer, :null => false
@@ -505,6 +516,11 @@ ActiveRecord::Schema.define do
t.string :type
end
+ create_table :randomly_named_table, :force => true do |t|
+ t.string :some_attribute
+ t.integer :another_attribute
+ end
+
create_table :ratings, :force => true do |t|
t.integer :comment_id
t.integer :value
@@ -596,6 +612,12 @@ ActiveRecord::Schema.define do
t.datetime :ending
end
+ create_table :teapots, :force => true do |t|
+ t.string :name
+ t.string :type
+ t.timestamps
+ end
+
create_table :topics, :force => true do |t|
t.string :title
t.string :author_name
@@ -607,8 +629,10 @@ ActiveRecord::Schema.define do
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
t.string :content, :limit => 4000
+ t.string :important, :limit => 4000
else
t.text :content
+ t.text :important
end
t.boolean :approved, :default => true
t.integer :replies_count, :default => 0
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index a39794fa39..60fea46fd3 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -1,4 +1,4 @@
-require 'logger'
+require 'active_support/logger'
require_dependency 'models/course'
module ARTest
@@ -12,9 +12,9 @@ module ARTest
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'
+ ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log")
+ ActiveRecord::Model.configurations = connection_config
+ ActiveRecord::Model.establish_connection 'arunit'
Course.establish_connection 'arunit2'
end
end