diff options
Diffstat (limited to 'activerecord')
393 files changed, 13862 insertions, 7465 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 460eeb5dfd..7d7870fe13 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -4,1806 +4,1089 @@ *Sergey Potapov* -* Properly detect if a connection is still active before using it - in multi-threaded environments. - - Fixes #12867. - - *Kevin Casey*, *Matthew Draper*, *William (B.J.) Snow Orvis* - -* When inverting add_index use the index name if present instead of - the columns. - - If there are two indices with matching columns and one of them is - explicitly named then reverting the migration adding the named one - would instead drop the unnamed one. - - The inversion of add_index will now drop the index by its name if - it is present. - - *Hubert Dąbrowski* - -* Add flag to disable schema dump after migration. - - Add a config parameter on Active Record named `dump_schema_after_migration` - which is true by default. Now schema dump does not happen at the - end of migration rake task if `dump_schema_after_migration` is false. - - *Emil Soman* - -* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now - return an `Enumerator` that can calculate its size. - - See also #13938. - - *Marc-André Lafortune* - -* Make sure transaction state gets reset after a commit operation on the record. - - If a new transaction was open inside a callback, the record was loosing track - of the transaction level state, and it was leaking that state. - - Fixes #12566. - - *arthurnn* - -* Pass `has_and_belongs_to_many` `:autosave` option to - the underlying `has_many :through` association. - - Fixes #13923. - - *Yves Senn* - -* PostgreSQL implementation of `SchemaStatements#index_name_exists?`. - - The database agnostic implementation does not detect with indexes that are - not supported by the ActiveRecord schema dumper. For example, expressions - indexes would not be detected. - - Fixes #11018. - - *Jonathan Baudanza* - -* Parsing PostgreSQL arrays with empty strings now works correctly. - - Previously, if you tried to parse `{"1","","2","","3"}` the result - would be `["1","2","3"]`, removing the empty strings from the array, - which would be incorrect. Now it will correctly produce `["1","","2","","3"]` - as the result of parsing the above PostgreSQL array. - - Fixes #13907. - - *Maurício Linhares* - -* Associations now raise `ArgumentError` on name conflicts. - - Dangerous association names conflicts include instance or class methods already - defined by `ActiveRecord::Base`. +* `index_exists?` with `:name` option does verify specified columns. Example: - class Car < ActiveRecord::Base - has_many :errors - end - # Will raise ArgumentError. - - Fixes #13217. - - *Lauro Caetano* - -* Fix regressions on `select_*` methods. - When `select_*` methods receive a `Relation` object, they should be able to - get the arel/binds from it. - Also fix regressions on `select_rows` that was ignoring the binds. - - Fixes #7538, #12017, #13731, #12056. - - *arthurnn* - -* Active Record objects can now be correctly dumped, loaded and dumped again - without issues. - - Previously, if you did `YAML.dump`, `YAML.load` and then `YAML.dump` again - in an Active Record model that used serialization it would fail at the last - dump due to the fields not being correctly serialized before being dumped - to YAML. Now it is possible to dump and load the same object as many times - as needed without any issues. - - Fixes #13861. - - *Maurício Linhares* - -* `find_in_batches` now returns an `Enumerator` when called without a block, so that it - can be chained with other `Enumerable` methods. - - *Marc-André Lafortune* - -* `enum` now raises on "dangerous" name conflicts. - - Dangerous name conflicts includes instance or class method conflicts - with methods defined within `ActiveRecord::Base` but not its ancestors, - as well as conflicts with methods generated by other enums on the same - class. - - Fixes #13389. - - *Godfrey Chan* - -* `scope` now raises on "dangerous" name conflicts. + add_index :articles, :title, name: "idx_title" - Similar to dangerous attribute methods, a scope name conflict is - dangerous if it conflicts with an existing class method defined within - `ActiveRecord::Base` but not its ancestors. - - See also #13389. - - *Godfrey Chan*, *Philippe Creux* - -* Correctly send an user provided statement to a `lock!()` call. - - person.lock! 'FOR SHARE NOWAIT' - # Before: SELECT * ... LIMIT 1 FOR UPDATE - # After: SELECT * ... LIMIT 1 FOR SHARE NOWAIT - - Fixes #13788. - - *Maurício Linhares* - -* Handle aliased attributes `select()`, `order()` and `reorder()`. - - *Tsutomu Kuroda* - -* Reset the collection association when calling `reset` on it. - - Before: - - post.comments.loaded? # => true - post.comments.reset - post.comments.loaded? # => true - - After: - - post.comments.loaded? # => true - post.comments.reset - post.comments.loaded? # => false - - Fixes #13777. - - *Kelsey Schlarman* - -* Make enum fields work as expected with the `ActiveModel::Dirty` API. - - Before this change, using the dirty API would have surprising results: + # Before: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `true` - conversation = Conversation.new - conversation.status = :active - conversation.status = :archived - conversation.status_was # => 0 + # After: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `false` - After this change, the same code would result in: + *Yves Senn*, *Matthew Draper* - conversation = Conversation.new - conversation.status = :active - conversation.status = :archived - conversation.status_was # => "active" +* When calling `update_columns` on a record that is not persisted, the error + message now reflects whether that object is a new record or has been + destroyed. - *Rafael Mendonça França* + *Lachlan Sylvester* -* `has_one` and `belongs_to` accessors don't add ORDER BY to the queries - anymore. +* Define `id_was` to get the previous value of the primary key. - Since Rails 4.0, we add an ORDER BY in the `first` method to ensure - consistent results among different database engines. But for singular - associations this behavior is not needed since we will have one record to - return. As this ORDER BY option can lead some performance issues we are - removing it for singular associations accessors. + Currently when we call id_was and we have a custom primary key name + Active Record will return the current value of the primary key. This + make impossible to correctly do an update operation if you change the + id. - Fixes #12623. + Fixes #16413. *Rafael Mendonça França* -* Prepend table name for column names passed to `Relation#select`. - - Example: - - Post.select(:id) - # Before: => SELECT id FROM "posts" - # After: => SELECT "posts"."id" FROM "posts" +* Deprecate `DatabaseTasks.load_schema` to act on the current connection. + Use `.load_schema_current` instead. In the future `load_schema` will + require the `configuration` to act on as an argument. *Yves Senn* -* Fail early with "Primary key not included in the custom select clause" - in `find_in_batches`. +* Fixed automatic maintaining test schema to properly handle sql structure + schema format. - Before this patch, the exception was raised after the first batch was - yielded to the block. This means that you only get it, when you hit the - `batch_size` treshold. This could shadow the issue in development. + Fixes #15394. - *Alexander Balashov* + *Wojciech Wnętrzak* -* Ensure `second` through `fifth` methods act like the `first` finder. +* Fix type casting to Decimal from Float with large precision. - The famous ordinal Array instance methods defined in ActiveSupport - (`first`, `second`, `third`, `fourth`, and `fifth`) are now available as - full-fledged finders in ActiveRecord. The biggest benefit of this is ordering - of the records returned now defaults to the table's primary key in ascending order. + *Tomohiro Hashidate* - Fixes #13743. +* Deprecate `Reflection#source_macro` - Example: + `Reflection#source_macro` is no longer needed in Active Record + source so it has been deprecated. Code that used `source_macro` + was removed in #16353. - User.all.second + *Eileen M. Uchtitelle*, *Aaron Patterson* - # Before - # => 'SELECT "users".* FROM "users"' +* No verbose backtrace by db:drop when database does not exist. - # After - # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1' + Fixes #16295. - User.offset(3).second + *Kenn Ejima* - # Before - # => 'SELECT "users".* FROM "users" LIMIT -1 OFFSET 3' # sqlite3 gem - # => 'SELECT "users".* FROM "users" OFFSET 3' # pg gem - # => 'SELECT `users`.* FROM `users` LIMIT 18446744073709551615 OFFSET 3' # mysql2 gem - - # After - # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 4' - - *Jason Meller* - -* ActiveRecord states are now correctly restored after a rollback for - models that did not define any transactional callbacks (i.e. - `after_commit`, `after_rollback` or `after_create`). - - Fixes #13744. - - *Godfrey Chan* - -* Make `touch` fire the `after_commit` and `after_rollback` callbacks. - - *Harry Brundage* - -* Enable partial indexes for `sqlite >= 3.8.0`. - - See http://www.sqlite.org/partialindex.html - - *Cody Cutrer* - -* Don't try to get the subclass if the inheritance column doesn't exist - - The `subclass_from_attrs` method is called even if the column specified by - the `inheritance_column` setting doesn't exist. This prevents setting associations - via the attributes hash if the association name clashes with the value of the setting, - typically `:type`. This worked previously in Rails 3.2. - - *Ujjwal Thaakar* - -* Enum mappings are now exposed via class methods instead of constants. +* Add support for Postgresql JSONB. Example: - class Conversation < ActiveRecord::Base - enum status: [ :active, :archived ] + create_table :posts do |t| + t.jsonb :meta_data end - Before: - - Conversation::STATUS # => { "active" => 0, "archived" => 1 } - - After: - - Conversation.statuses # => { "active" => 0, "archived" => 1 } - - *Godfrey Chan* - -* Set `NameError#name` when STI-class-lookup fails. - - *Chulki Lee* - -* Fix bug in `becomes!` when changing from the base model to a STI sub-class. + *Philippe Creux*, *Chris Teague* - Fixes #13272. - - *the-web-dev*, *Yves Senn* - -* Currently Active Record can be configured via the environment variable - `DATABASE_URL` or by manually injecting a hash of values which is what Rails does, - reading in `database.yml` and setting Active Record appropriately. Active Record - expects to be able to use `DATABASE_URL` without the use of Rails, and we cannot - rip out this functionality without deprecating. This presents a problem though - when both config is set, and a `DATABASE_URL` is present. Currently the - `DATABASE_URL` should "win" and none of the values in `database.yml` are - used. This is somewhat unexpected, if one were to set values such as - `pool` in the `production:` group of `database.yml` they are ignored. - - There are many ways that Active Record initiates a connection today: - - - Stand Alone (without rails) - - `rake db:<tasks>` - - `ActiveRecord.establish_connection` - - - With Rails - - `rake db:<tasks>` - - `rails <server> | <console>` - - `rails dbconsole` - - Now all of these behave exactly the same way. The best way to do - this is to put all of this logic in one place so it is guaranteed to be used. - - Here is the matrix of how this behavior works: - - ``` - No database.yml - No DATABASE_URL - => Error - ``` - - ``` - database.yml present - No DATABASE_URL - => Use database.yml configuration - ``` - - ``` - No database.yml - DATABASE_URL present - => use DATABASE_URL configuration - ``` - - ``` - database.yml present - DATABASE_URL present - => Merged into `url` sub key. If both specify `url` sub key, the `database.yml` `url` - sub key "wins". If other paramaters `adapter` or `database` are specified in YAML, - they are discarded as the `url` sub key "wins". - ``` - - Current implementation uses `ActiveRecord::Base.configurations` to resolve and merge - all connection information before returning. This is achieved through a utility - class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`. - - To understand the exact behavior of this class, it is best to review the - behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`. - - *Richard Schneeman* - -* Make `change_column_null` revertable. Fixes #13576. - - *Yves Senn*, *Nishant Modak*, *Prathamesh Sonpatki* - -* Don't create/drop the test database if RAILS_ENV is specified explicitly. - - Previously, when the environment was development, we would always - create or drop both the test and development databases. - - Now, if RAILS_ENV is explicitly defined as development, we don't create - the test database. - - *Damien Mathieu* - -* Initialize version on Migration objects so that it can be used in a migration, - and it will be included in the announce message. - - *Dylan Thacker-Smith* - -* `change_table` now uses the current adapter's `update_table_definition` - method to retrieve a specific table definition. - This ensures that `change_table` and `create_table` will use - similar objects. - - Fixes #13577, #13503. - - *Nishant Modak*, *Prathamesh Sonpatki*, *Rafael Mendonça França* - -* Fixed ActiveRecord::Store nil conversion TypeError when using YAML coder. - In case the YAML passed as paramter is nil, uses an empty string. - - Fixes #13570. - - *Thales Oliveira* - -* Deprecate unused `ActiveRecord::Base.symbolized_base_class` - and `ActiveRecord::Base.symbolized_sti_name` without replacement. +* `db:purge` with MySQL respects `Rails.env`. *Yves Senn* -* Since the `test_help.rb` file in Railties now automatically maintains - your test schema, the `rake db:test:*` tasks are deprecated. This - doesn't stop you manually running other tasks on your test database - if needed: - - rake db:schema:load RAILS_ENV=test - - *Jon Leighton* - -* Fix presence validator for association when the associated record responds to `to_a`. - - *gmarik* - -* Fixed regression on preload/includes with multiple arguments failing in certain conditions, - raising a NoMethodError internally by calling `reflect_on_association` for `NilClass:Class`. - - Fixes #13437. - - *Vipul A M*, *khustochka* - -* Add the ability to nullify the `enum` column. - - Example: +* `change_column_default :table, :column, nil` with PostgreSQL will issue a + `DROP DEFAULT` instead of a `DEFAULT NULL` query. - class Conversation < ActiveRecord::Base - enum gender: [:female, :male] - end + Fixes #16261. - Conversation::GENDER # => { female: 0, male: 1 } + *Matthew Draper*, *Yves Senn* - # conversation.update! gender: 0 - conversation.female! - conversation.female? # => true - conversation.gender # => "female" - - # conversation.update! gender: nil - conversation.gender = nil - conversation.gender.nil? # => true - conversation.gender # => nil - - *Amr Tamimi* - -* Connection specification now accepts a "url" key. The value of this - key is expected to contain a database URL. The database URL will be - expanded into a hash and merged. - - *Richard Schneeman* - -* An `ArgumentError` is now raised on a call to `Relation#where.not(nil)`. +* Allow to specify a type for the foreign key column in `references` + and `add_reference`. Example: - User.where.not(nil) - - # Before - # => 'SELECT `users`.* FROM `users` WHERE (NOT (NULL))' - - # After - # => ArgumentError, 'Invalid argument for .where.not(), got nil.' - - *Kuldeep Aggarwal* - -* Deprecated use of string argument as a configuration lookup in - `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. - - *José Valim* - -* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize - values for `array`, `hstore` and `json` column types in PostgreSQL. - - Fixes #12261. - - *Tadas Tamosauskas*, *Carlos Antonio da Silva* - -* Do not consider PostgreSQL array columns as number or text columns. - - The code uses these checks in several places to know what to do with a - particular column, for instance AR attribute query methods has a branch - like this: - - if column.number? - !value.zero? + change_table :vehicle do |t| + t.references :station, type: :uuid end - This should never be true for array columns, since it would be the same - as running [].zero?, which results in a NoMethodError exception. - - Fixing this by ensuring that array columns in PostgreSQL never return - true for number?/text? checks. - - *Carlos Antonio da Silva* - -* When connecting to a non-existant database, the error: - `ActiveRecord::NoDatabaseError` will now be raised. When being used with Rails - the error message will include information on how to create a database: - `rake db:create`. Supported adapters: postgresql, mysql, mysql2, sqlite3 - - *Richard Schneeman* - -* Do not raise `'cannot touch on a new record object'` exception on destroying - already destroyed `belongs_to` association with `touch: true` option. - - Fixes #13445. - - Example: - - # Given Comment has belongs_to :post, touch: true - comment.post.destroy - comment.destroy # no longer raises an error - - *Paul Nikitochkin* - -* Fix a bug when assigning an array containing string numbers to a - PostgreSQL integer array column. - - Fixes #13444. - - Example: - - # Given Book#ratings is of type :integer, array: true - Book.new(ratings: [1, 2]) # worked before - Book.new(ratings: ['1', '2']) # now works as well - - *Damien Mathieu* - -* Improve the default select when `from` is used. - - Previously, if you did something like Topic.from(:temp_topics), it - would generate SQL like: - - SELECT topics.* FROM temp_topics; - - Which is will cause an error since there's not a topics table to select - from. + *Andrey Novikov*, *Łukasz Sarnacki* - Now the default if you use from is just `*`: +* `create_join_table` removes a common prefix when generating the join table. + This matches the existing behavior of HABTM associations. - SELECT * FROM temp_topics; + Fixes #13683. - *Cody Cutrer* + *Stefan Kanev* -* Fix `PostgreSQL` insert to properly extract table name from multiline string SQL. +* Dont swallow errors on compute_type when having a bad alias_method on + a class. - Previously, executing an insert SQL in `PostgreSQL` with a command like this: - - insert into articles( - number) - values( - 5152 - ) + *arthurnn* - would not work because the adapter was unable to extract the correct `articles` - table name. +* PostgreSQL invalid `uuid` are convert to nil. - *Kuldeep Aggarwal* + *Abdelkader Boudih* -* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert - to an `Array` by calling `#to_a` before using these methods. +* Restore 4.0 behavior for using serialize attributes with `JSON` as coder. - It intends to prevent odd bugs and confusion in code that call mutator - methods directly on the `Relation`. + With 4.1.x, `serialize` started returning a string when `JSON` was passed as + the second attribute. It will now return a hash as per previous versions. Example: - # Instead of this - Author.where(name: 'Hank Moody').compact! - - # Now you have to do this - authors = Author.where(name: 'Hank Moody').to_a - authors.compact! - - *Lauro Caetano* - -* Better support for `where()` conditions that use a `belongs_to` - association name. - - Using the name of an association in `where` previously worked only - if the value was a single `ActiveRecord::Base` object. e.g. - - Post.where(author: Author.first) - - Any other values, including `nil`, would cause invalid SQL to be - generated. This change supports arguments in the `where` query - conditions where the key is a `belongs_to` association name and the - value is `nil`, an `Array` of `ActiveRecord::Base` objects, or an - `ActiveRecord::Relation` object. - class Post < ActiveRecord::Base - belongs_to :author + serialize :comment, JSON end - `nil` value finds records where the association is not set: - - Post.where(author: nil) - # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IS NULL - - `Array` values find records where the association foreign key - matches the ids of the passed ActiveRecord models, resulting - in the same query as `Post.where(author_id: [1,2])`: - - authors_array = [Author.find(1), Author.find(2)] - Post.where(author: authors_array) - # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (1, 2) - - `ActiveRecord::Relation` values find records using the same - query as `Post.where(author_id: Author.where(last_name: "Emde"))` - - Post.where(author: Author.where(last_name: "Emde")) - # SELECT "posts".* FROM "posts" - # WHERE "posts"."author_id" IN ( - # SELECT "authors"."id" FROM "authors" - # WHERE "authors"."last_name" = 'Emde') - - Polymorphic `belongs_to` associations will continue to be handled - appropriately, with the polymorphic `association_type` field added - to the query to match the base class of the value. This feature - previously only worked when the value was a single `ActveRecord::Base`. - - class Post < ActiveRecord::Base - belongs_to :author, polymorphic: true + class Comment + include ActiveModel::Model + attr_accessor :category, :text end - Post.where(author: Author.where(last_name: "Emde")) - # Generates a query similar to: - Post.where(author_id: Author.where(last_name: "Emde"), author_type: "Author") - - *Martin Emde* - -* Respect temporary option when dropping tables with MySQL. + post = Post.create! + post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.") + post.save! - Normal DROP TABLE also works, but commits the transaction. + # 4.0 + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} - drop_table :temporary_table, temporary: true + # 4.1 before + post.comment # => "#<Comment:0x007f80ab48ff98>" - *Cody Cutrer* + # 4.1 after + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} -* Add option to create tables from a query. + When using `JSON` as the coder in `serialize`, Active Record will use the + new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to + `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped + correctly using the `#as_json` hook. - create_table(:long_query, temporary: true, - as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id") + To keep the previous behaviour, supply a custom coder instead + ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)). - Generates: + Fixes #15594. - CREATE TEMPORARY TABLE long_query AS - SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id + *Jenn Cooper* - *Cody Cutrer* +* Do not use `RENAME INDEX` syntax for MariaDB 10.0. -* `db:test:clone` and `db:test:prepare` must load Rails environment. + Fixes #15931. - `db:test:clone` and `db:test:prepare` use `ActiveRecord::Base`. configurations, - so we need to load the Rails environment, otherwise the config wont be in place. + *Jeff Browning* - *arthurnn* +* Calling `#empty?` on a `has_many` association would use the value from the + counter cache if one exists. -* Use the right column to type cast grouped calculations with custom expressions. + *David Verhasselt* - Fixes #13230. +* Fix the schema dump generated for tables without constraints and with + primary key with default value of custom PostgreSQL function result. - Example: + Fixes #16111. - # Before - Account.group(:firm_name).sum('0.01 * credit_limit') - # => { '37signals' => '0.5' } + *Andrey Novikov* - # After - Account.group(:firm_name).sum('0.01 * credit_limit') - # => { '37signals' => 0.5 } +* Fix the SQL generated when a `delete_all` is run on an association to not + produce an `IN` statements. - *Paul Nikitochkin* - -* Polymorphic `belongs_to` associations with the `touch: true` option set update the timestamps of - the old and new owner correctly when moved between owners of different types. - - Example: - - class Rating < ActiveRecord::Base - belongs_to :rateable, polymorphic: true, touch: true - end - - rating = Rating.create rateable: Song.find(1) - rating.update_attributes rateable: Book.find(2) # => timestamps of Song(1) and Book(2) are updated - - *Severin Schoepke* - -* Improve formatting of migration exception messages: make them easier to read - with line breaks before/after, and improve the error for pending migrations. - - *John Bachir* - -* Fix `last` with `offset` to return the proper record instead of always the last one. + Before: - Example: + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2) - Model.offset(4).last - # => returns the 4th record from the end. + After: - Fixes #7441. + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 - *kostya*, *Lauro Caetano* + *Eileen M. Uchitelle, Aaron Patterson* -* `type_to_sql` returns a `String` for unmapped columns. This fixes an error - when using unmapped PostgreSQL array types. +* Avoid type casting boolean and ActiveSupport::Duration values to numeric + values for string columns. Otherwise, in some database, the string column + values will be coerced to a numeric allowing false or 0.seconds match any + string starting with a non-digit. Example: - change_colum :table, :column, :bigint, array: true + App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0' - Fixes #13146. - - *Jens Fahnenbruck*, *Yves Senn* + *Dylan Thacker-Smith* -* Fix `QueryCache` to work with nested blocks, so that it will only clear the existing cache - after leaving the outer block instead of clearing it right after the inner block is finished. +* Add a `:required` option to singular associations, providing a nicer + API for presence validations on associations. - *Vipul A M* + *Sean Griffin* -* The ERB in fixture files is no longer evaluated in the context of the main - object. Helper methods used by multiple fixtures should be defined on the - class object returned by `ActiveRecord::FixtureSet.context_class`. +* Fixed error in `reset_counters` when associations have `select` scope. + (Call to `count` generates invalid SQL.) - *Victor Costan* + *Cade Truitt* -* Previously, the `has_one` macro incorrectly accepted the `counter_cache` - option, but never actually supported it. Now it will raise an `ArgumentError` - when using `has_one` with `counter_cache`. +* After a successful `reload`, `new_record?` is always false. - *Godfrey Chan* + Fixes #12101. -* Implement `rename_index` natively for MySQL >= 5.7. + *Matthew Draper* - *Cody Cutrer* +* PostgreSQL renaming table doesn't attempt to rename non existent sequences. -* Fix bug when validating the uniqueness of an aliased attribute. + *Abdelkader Boudih* - Fixes #12402. +* Move 'dependent: :destroy' handling for 'belongs_to' + from 'before_destroy' to 'after_destroy' callback chain - *Lauro Caetano* + Fixes #12380. -* Update counter cache on a `has_many` relationship regardless of default scope. + *Ivan Antropov* - Fixes #12952. +* Detect in-place modifications on String attributes. - *Uku Taht* + Before this change user have to mark the attribute as changed to it be persisted + in the database. Now it is not required anymore. -* `rename_index` adds the new index before removing the old one. This allows to - rename indexes on columns with a foreign key and prevents the following error: + Before: - Cannot drop index 'index_engines_on_car_id': needed in a foreign key constraint + user = User.first + user.name << ' Griffin' + user.name_will_change! + user.save + user.reload.name # => "Sean Griffin" - *Cody Cutrer*, *Yves Senn* + After: -* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child - marked with `dependent: destroy` fails to be destroyed. + user = User.first + user.name << ' Griffin' + user.save + user.reload.name # => "Sean Griffin" - Fixex #12812. + *Sean Griffin* - *Brian Thomas Storti* +* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record + is invalid. -* Fix validation on uniqueness of empty association. + *Bogdan Gusiev*, *Marc Schütz* - *Evgeny Li* +* Support for adding and removing foreign keys. Foreign keys are now + a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter + and PostgreSQLAdapter. -* Make `ActiveRecord::Relation#unscope` affect relations it is merged in to. + Many thanks to *Matthew Higgins* for laying the foundation with his work on + [foreigner](https://github.com/matthuhiggins/foreigner). - *Jon Leighton* + Example: -* Use strings to represent non-string `order_values`. + # within your migrations: + add_foreign_key :articles, :authors + remove_foreign_key :articles, :authors *Yves Senn* -* Checks to see if the record contains the foreign key to set the inverse automatically. +* Fix subtle bugs regarding attribute assignment on models with no primary + key. `'id'` will no longer be part of the attributes hash. - *Edo Balvers* + *Sean Griffin* -* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from a model's attribute or method. +* Deprecate automatic counter caches on `has_many :through`. The behavior was + broken and inconsistent. - Example: + *Sean Griffin* - class User < ActiveRecord::Base - to_param :name - end +* `preload` preserves readonly flag for associations. - user = User.find_by(name: 'Fancy Pants') - user.id # => 123 - user.to_param # => "123-fancy-pants" + See #15853. - *Javan Makhmali* + *Yves Senn* -* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models. +* Assume numeric types have changed if they were assigned to a value that + would fail numericality validation, regardless of the old value. Previously + this would only occur if the old value was 0. Example: - Post.no_touching do - Post.first.touch - end + model = Model.create!(number: 5) + model.number = '5wibble' + model.number_changed? # => true - *Sam Stephenson*, *Damien Mathieu* + Fixes #14731. -* Prevent the counter cache from being decremented twice when destroying - a record on a `has_many :through` association. + *Sean Griffin* - Fixes #11079. +* `reload` no longer merges with the existing attributes. + The attribute hash is fully replaced. The record is put into the same state + as it would be with `Model.find(model.id)`. - *Dmitry Dedov* + *Sean Griffin* -* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`. - `type_cast` will return `1` for `true` and `0` for `false`. +* The object returned from `select_all` must respond to `column_types`. + If this is not the case a `NoMethodError` is raised. - Fixes #11119. + *Sean Griffin* - *Adam Williams*, *Yves Senn* +* `has_many :through` associations will no longer save the through record + twice when added in an `after_create` callback defined before the + associations. -* Fix bug where `has_one` association record update result in crash, when replaced with itself. + Fixes #3798. - Fixes #12834. + *Sean Griffin* - *Denis Redozubov*, *Sergio Cambra* +* Detect in-place modifications of PG array types -* Log bind variables after they are type casted. This makes it more - transparent what values are actually sent to the database. + *Sean Griffin* - irb(main):002:0> Event.find("im-no-integer") - # Before: ... WHERE "events"."id" = $1 LIMIT 1 [["id", "im-no-integer"]] - # After: ... WHERE "events"."id" = $1 LIMIT 1 [["id", 0]] +* Add `bin/rake db:purge` task to empty the current database. *Yves Senn* -* Fix uninitialized constant `TransactionState` error when `Marshall.load` is used on an Active Record result. +* Deprecate `serialized_attributes` without replacement. - Fixes #12790. + *Sean Griffin* - *Jason Ayre* +* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets + are part of the URI structure, not the actual host. -* `.unscope` now removes conditions specified in `default_scope`. + Fixes #15705. - *Jon Leighton* + *Andy Bakun*, *Aaron Stone* -* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing, named where condition. +* Ensure both parent IDs are set on join records when both sides of a + through association are new. - Examples: + *Sean Griffin* - Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0 - Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0 - Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0 +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. - *DHH* + Fixes #8328. -* Extend `ActiveRecord::Base#cache_key` to take an optional list of timestamp attributes of which the highest will be used. + *Sean Griffin* - Example: +* Pluck now works when selecting columns from different tables with the same + name. - # last_reviewed_at will be used, if that's more recent than updated_at, or vice versa - Person.find(5).cache_key(:updated_at, :last_reviewed_at) + Fixes #15649. - *DHH* + *Sean Griffin* -* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values map to integers in the database, but can be queried by name. +* Remove `cache_attributes` and friends. All attributes are cached. - Example: + *Sean Griffin* - class Conversation < ActiveRecord::Base - enum status: [:active, :archived] - end - - Conversation::STATUS # => { active: 0, archived: 1 } - - # conversation.update! status: 0 - conversation.active! - conversation.active? # => true - conversation.status # => "active" - - # conversation.update! status: 1 - conversation.archived! - conversation.archived? # => true - conversation.status # => "archived" +* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. - # conversation.update! status: 1 - conversation.status = :archived + *Akshay Vishnoi* - *DHH* +* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as + `Enumerable#find` does. -* `ActiveRecord::Base#attribute_for_inspect` now truncates long arrays (more than 10 elements). + Fixes #15382. - *Jan Bernacki* + *James Yang* -* Allow for the name of the `schema_migrations` table to be configured. +* Make timezone aware attributes work with PostgreSQL array columns. - *Jerad Phelps* + Fixes #13402. -* Do not add to scope includes values from through associations. - Fixed bug when providing `includes` in through association scope, and fetching targets. + *Kuldeep Aggarwal*, *Sean Griffin* - Example: - - class Vendor < ActiveRecord::Base - has_many :relationships, -> { includes(:user) } - has_many :users, through: :relationships - end +* `ActiveRecord::SchemaMigration` has no primary key regardless of the + `primary_key_prefix_type` configuration. - vendor = Vendor.first + Fixes #15051. - # Before - - vendor.users.to_a # => Raises exception: not found `:user` for `User` - - # After - - vendor.users.to_a # => No exception is raised - - Fixes #12242, #9517, #10240. - - *Paul Nikitochkin* + *JoseLuis Torres*, *Yves Senn* -* Type cast json values on write, so that the value is consistent - with reading from the database. - - Example: - - x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar} - - # Before: - x.tags # => {"string" => "foo", :symbol => :bar} - - # After: - x.tags # => {"string" => "foo", "symbol" => "bar"} - - *Severin Schoepke* - -* `ActiveRecord::Store` works together with PostgreSQL `hstore` columns. - - Fixes #12452. - - *Yves Senn* +* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`. -* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of - all registered `stored_attributes`. Now every subclass of - `ActiveRecord::Base` has it's own `Hash`. + Fixes #15538. *Yves Senn* -* Save `has_one` association when primary key is manually set. +* Baseclass becomes! subclass. - Fixes #12302. + Before this change, a record which changed its STI type, could not be + updated. - *Lauro Caetano* - -* Allow any version of BCrypt when using `has_secure_password`. - - *Mike Perham* - -* Sub-query generated for `Relation` passed as array condition did not take in account - bind values and have invalid syntax. - - Generate sub-query with inline bind values. - - Fixes #12586. - - *Paul Nikitochkin* - -* Fix a bug where rake db:structure:load crashed when the path contained - spaces. - - *Kevin Mook* - -* `ActiveRecord::QueryMethods#unscope` unscopes negative equality - - Allows you to call `#unscope` on a relation with negative equality - operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have - been generated through the use of `where.not`. - - *Eric Hankins* - -* Raise an exception when model without primary key calls `.find_with_ids`. + Fixes #14785. - *Shimpei Makimoto* + *Matthew Draper*, *Earl St Sauver*, *Edo Balvers* -* Make `Relation#empty?` use `exists?` instead of `count`. +* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the + `proper_table_name` instance method on `ActiveRecord::Migration` instead. - *Szymon Nowak* + *Akshay Vishnoi* -* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`. +* Fix regression on eager loading association based on SQL query rather than + existing column. - *Kenta Okamoto* + Fixes #15480. -* `NullRelation#pluck` takes a list of columns + *Lauro Caetano*, *Carlos Antonio da Silva* - The method signature in `NullRelation` was updated to mimic that in - `Calculations`. +* Deprecate returning `nil` from `column_for_attribute` when no column exists. + It will return a null object in Rails 5.0 - *Derek Prior* + *Sean Griffin* -* `scope_chain` should not be mutated for other reflections. +* Implemented ActiveRecord::Base#pretty_print to work with PP. - Currently `scope_chain` uses same array for building different - `scope_chain` for different associations. During processing - these arrays are sometimes mutated and because of in-place - mutation the changed `scope_chain` impacts other reflections. + *Ethan* - Fix is to dup the value before adding to the `scope_chain`. - - Fixes #3882. - - *Neeraj Singh* - -* Prevent the inversed association from being reloaded on save. - - Fixes #9499. - - *Dmitry Polushkin* - -* Generate subquery for `Relation` if it passed as array condition for `where` - method. - - Example: - - # Before - Blog.where('id in (?)', Blog.where(id: 1)) - # => SELECT "blogs".* FROM "blogs" WHERE "blogs"."id" = 1 - # => SELECT "blogs".* FROM "blogs" WHERE (id IN (1)) - - # After - Blog.where('id in (?)', Blog.where(id: 1).select(:id)) - # => SELECT "blogs".* FROM "blogs" - # WHERE "blogs"."id" IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."id" = 1) - - Fixes #12415. - - *Paul Nikitochkin* +* Preserve type when dumping PostgreSQL point, bit, bit varying and money + columns. -* For missed association exception message - which is raised in `ActiveRecord::Associations::Preloader` class - added owner record class name in order to simplify to find problem code. + *Yves Senn* - *Paul Nikitochkin* +* New records remain new after YAML serialization. -* `has_and_belongs_to_many` is now transparently implemented in terms of - `has_many :through`. Behavior should remain the same, if not, it is a bug. + *Sean Griffin* -* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept - a savepoint name. +* PostgreSQL support default values for enum types. Fixes #7814. *Yves Senn* -* Make `next_migration_number` accessible for third party generators. +* PostgreSQL `default_sequence_name` respects schema. Fixes #7516. *Yves Senn* -* Objects instantiated using a null relationship will now retain the - attributes of the where clause. +* Fixed `columns_for_distinct` of postgresql adapter to work correctly + with orders without sort direction modifiers. - Fixes #11676, #11675, #11376. + *Nikolay Kondratyev* - *Paul Nikitochkin*, *Peter Brown*, *Nthalk* +* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719. -* Fixed `ActiveRecord::Associations::CollectionAssociation#find` - when using `has_many` association with `:inverse_of` and finding an array of one element, - it should return an array of one element too. - - *arthurnn* - -* Callbacks on has_many should access the in memory parent if a inverse_of is set. - - *arthurnn* - -* `ActiveRecord::ConnectionAdapters.string_to_time` respects - string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST). - - Fixes #12278. - - *kennyj* - -* Calling `update_attributes` will now throw an `ArgumentError` whenever it - gets a `nil` argument. More specifically, it will throw an error if the - argument that it gets passed does not respond to to `stringify_keys`. - - Example: - - @my_comment.update_attributes(nil) # => raises ArgumentError - - *John Wang* - -* Deprecate `quoted_locking_column` method, which isn't used anywhere. - - *kennyj* - -* Migration dump UUID default functions to schema.rb. + *Yves Senn* - Fixes #10751. +* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`. + Fixes duplication in combination with `store_accessor`. - *kennyj* + Fixes #15369. -* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan` - when using `has_many` association with `:inverse_of` option and UUID primary key. + *Yves Senn* - Fixes #10450. +* `rake railties:install:migrations` respects the order of railties. - *kennyj* + *Arun Agrawal* -* Fix: joins association, with defined in the scope block constraints by using several - where constraints and at least of them is not `Arel::Nodes::Equality`, - generates invalid SQL expression. +* Fix redefine a has_and_belongs_to_many inside inherited class + Fixing regression case, where redefining the same has_an_belongs_to_many + definition into a subclass would raise. - Fixes #11963. + Fixes #14983. - *Paul Nikitochkin* + *arthurnn* -* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed - query to fetch results rather than loading the entire collection. +* Fix has_and_belongs_to_many public reflection. + When defining a has_and_belongs_to_many, internally we convert that to two has_many. + But as `reflections` is a public API, people expect to see the right macro. - *Lann Martin* + Fixes #14682. -* Make possible to run SQLite rake tasks without the `Rails` constant defined. + *arthurnn* - *Damien Mathieu* +* Fixed serialization for records with an attribute named `format`. -* Allow Relation#from to accept other relations with bind values. + Fixes #15188. - *Ryan Wallace* + *Godfrey Chan* -* Fix inserts with prepared statements disabled. +* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` + on a NullRelation should return a Hash. - Fixes #12023. + *Kuldeep Aggarwal* - *Rafael Mendonça França* +* Fixed serialized fields returning serialized data after being updated with + `update_column`. -* Setting a has_one association on a new record no longer causes an empty - transaction. + *Simon Hørup Eskildsen* - *Dylan Thacker-Smith* +* Fixed polymorphic eager loading when using a String as foreign key. -* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag. + Fixes #14734. - *thedarkone* + *Lauro Caetano* -* Re-use `order` argument pre-processing for `reorder`. +* Change belongs_to touch to be consistent with timestamp updates - *Paul Nikitochkin* + If a model is set up with a belongs_to: touch relationship the parent + record will only be touched if the record was modified. This makes it + consistent with timestamp updating on the record itself. -* Fix PredicateBuilder so polymorphic association keys in `where` clause can - accept objects other than direct descendants of `ActiveRecord::Base` (decorated - models, for example). + *Brock Trappitt* - *Mikhail Dieterle* +* Fixed the inferred table name of a has_and_belongs_to_many auxiliar + table inside a schema. -* PostgreSQL adapter recognizes negative money values formatted with - parentheses (eg. `($1.25) # => -1.25`)). - Fixes #11899. + Fixes #14824. - *Yves Senn* + *Eric Chahin* -* Stop interpreting SQL 'string' columns as :string type because there is no - common STRING datatype in SQL. +* Remove unused `:timestamp` type. Transparently alias it to `:datetime` + in all cases. Fixes inconsistencies when column types are sent outside of + `ActiveRecord`, such as for XML Serialization. - *Ben Woosley* + *Sean Griffin* -* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases. +* Fix bug that added `table_name_prefix` and `table_name_suffix` to + extension names in PostgreSQL when migrating. - *Xavier Noria* + *Joao Carlos* -* Assign inet/cidr attribute with `nil` value for invalid address. +* The `:index` option in migrations, which previously was only available for + `references`, now works with any column types. - Example: + *Marc Schütz* - record = User.new - record.logged_in_from_ip # is type of an inet or a cidr +* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`. - # Before: - record.logged_in_from_ip = 'bad ip address' # raise exception + *jnormore* - # After: - record.logged_in_from_ip = 'bad ip address' # do not raise exception - record.logged_in_from_ip # => nil - record.logged_in_from_ip_before_type_cast # => 'bad ip address' +* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having` + or `offset`. - *Paul Nikitochkin* + In these cases the generated query ignored them and that caused unintended + records to be deleted. -* `add_to_target` now accepts a second optional `skip_callbacks` argument + Fixes #11985. - If truthy, it will skip the :before_add and :after_add callbacks. + *Leandro Facchinetti* - *Ben Woosley* +* Floats with limit >= 25 that get turned into doubles in MySQL no longer have + their limit dropped from the schema. -* Fix interactions between `:before_add` callbacks and nested attributes - assignment of `has_many` associations, when the association was not - yet loaded: + Fixes #14135. - - A `:before_add` callback was being called when a nested attributes - assignment assigned to an existing record. + *Aaron Nelson* - - Nested Attributes assignment did not affect the record in the - association target when a `:before_add` callback triggered the - loading of the association +* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many + association. - *Jörg Schray* + Fixes #14709. -* Allow enable_extension migration method to be revertible. + *Kassio Borges* - *Eric Tipton* +* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and + strings in column names as equal. -* Type cast hstore values on write, so that the value is consistent - with reading from the database. + This fixes a rare case in which more bind values are passed than there are + placeholders for them in the generated SQL statement, which can make PostgreSQL + throw a `StatementInvalid` exception. - Example: + *Nat Budin* - x = Hstore.new tags: {"bool" => true, "number" => 5} +* Fix `stored_attributes` to correctly merge the details of stored + attributes defined in parent classes. - # Before: - x.tags # => {"bool" => true, "number" => 5} + Fixes #14672. - # After: - x.tags # => {"bool" => "true", "number" => "5"} + *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy* - *Yves Senn* , *Severin Schoepke* +* `change_column_default` allows `[]` as argument to `change_column_default`. -* Fix multidimensional PostgreSQL arrays containing non-string items. + Fixes #11586. *Yves Senn* -* Fixes bug when using includes combined with select, the select statement was overwritten. - - Fixes #11773. - - *Edo Balvers* - -* Load fixtures from linked folders. - - *Kassio Borges* +* Handle `name` and `"char"` column types in the PostgreSQL adapter. -* Create a directory for sqlite3 file if not present on the system. + `name` and `"char"` are special character types used internally by + PostgreSQL and are used by internal system catalogs. These field types + can sometimes show up in structure-sniffing queries that feature internal system + structures or with certain PostgreSQL extensions. - *Richard Schneeman* + *J Smith*, *Yves Senn* -* Removed redundant override of `xml` column definition for PostgreSQL, - in order to use `xml` column type instead of `text`. - - *Paul Nikitochkin*, *Michael Nikitochkin* - -* Revert `ActiveRecord::Relation#order` change that make new order - prepend the old one. +* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and + NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN` Before: - User.order("name asc").order("created_at desc") - # SELECT * FROM users ORDER BY created_at desc, name asc + Point.create(value: 1.0/0) + Point.last.value # => 0.0 After: - User.order("name asc").order("created_at desc") - # SELECT * FROM users ORDER BY name asc, created_at desc + Point.create(value: 1.0/0) + Point.last.value # => Infinity - This also affects order defined in `default_scope` or any kind of associations. + *Innokenty Mikhailov* -* Add ability to define how a class is converted to Arel predicates. - For example, adding a very vendor specific regex implementation: +* Allow the PostgreSQL adapter to handle bigserial primary key types again. - regex_handler = proc do |column, value| - Arel::Nodes::InfixOperation.new('~', column, value.source) - end - ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler) + Fixes #10410. - *Sean Griffin & @joannecheng* + *Patrick Robertson* -* Don't allow `quote_value` to be called without a column. +* Deprecate joining, eager loading and preloading of instance dependent + associations without replacement. These operations happen before instances + are created. The current behavior is unexpected and can result in broken + behavior. - Some adapters require column information to do their job properly. - By enforcing the provision of the column for this internal method - we ensure that those using adapters that require column information - will always get the proper behavior. + Fixes #15024. - *Ben Woosley* + *Yves Senn* -* When using optimistic locking, `update` was not passing the column to `quote_value` - to allow the connection adapter to properly determine how to quote the value. This was - affecting certain databases that use specific column types. +* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. - Fixes #6763. + has_and_belongs_to_many should fall back to using the normal CollectionAssociation's + size calculation if the collection is not cached or loaded. - *Alfred Wong* + Fixes #14913, #14914. -* rescue from all exceptions in `ConnectionManagement#call` + *Fred Wu* - Fixes #11497. +* Return a non zero status when running `rake db:migrate:status` and migration table does + not exist. - As `ActiveRecord::ConnectionAdapters::ConnectionManagement` middleware does - not rescue from Exception (but only from StandardError), the Connection - Pool quickly runs out of connections when multiple erroneous Requests come - in right after each other. + *Paul B.* - Rescuing from all exceptions and not just StandardError, fixes this - behaviour. +* Add support for module-level `table_name_suffix` in models. - *Vipul A M* + This makes `table_name_suffix` work the same way as `table_name_prefix` when + using namespaced models. -* `change_column` for PostgreSQL adapter respects the `:array` option. + *Jenner LaFave* - *Yves Senn* +* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0. -* Remove deprecation warning from `attribute_missing` for attributes that are columns. + In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`. + In 4.0 series it is delegated to `Array#join`. - *Arun Agrawal* + *Bogdan Gusiev* -* Remove extra decrement of transaction deep level. +* Log nil binary column values correctly. - Fixes #4566. + When an object with a binary column is updated with a nil value + in that column, the SQL logger would throw an exception when trying + to log that nil value. This only occurs when updating a record + that already has a non-nil value in that column since an initial nil + value isn't included in the SQL anyway (at least, when dirty checking + is enabled.) The column's new value will now be logged as `<NULL binary data>` + to parallel the existing `<N bytes of binary data>` for non-nil values. - *Paul Nikitochkin* + *James Coleman* -* Reset @column_defaults when assigning `locking_column`. - We had a potential problem. For example: +* Rails will now pass a custom validation context through to autosave associations + in order to validate child associations with the same context. - class Post < ActiveRecord::Base - self.column_defaults # if we call this unintentionally before setting locking_column ... - self.locking_column = 'my_locking_column' - end + Fixes #13854. - Post.column_defaults["my_locking_column"] - => nil # expected value is 0 ! + *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* - *kennyj* +* Stringify all variables keys of MySQL connection configuration. -* Remove extra select and update queries on save/touch/destroy ActiveRecord model - with belongs to reflection with option `touch: true`. + When `sql_mode` variable for MySQL adapters set in configuration as `String` + was ignored and overwritten by strict mode option. - Fixes #11288. + Fixes #14895. *Paul Nikitochkin* -* Remove deprecated nil-passing to the following `SchemaCache` methods: - `primary_keys`, `tables`, `columns` and `columns_hash`. +* Ensure SQLite3 statements are closed on errors. - *Yves Senn* + Fixes #13631. -* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`. + *Timur Alperovich* - *Yves Senn* +* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve. -* Remove deprecated String constructor from `ActiveRecord::Migrator`. + *Hector Satre* - *Yves Senn* +* When using a custom `join_table` name on a `habtm`, rails was not saving it + on Reflections. This causes a problem when rails loads fixtures, because it + uses the reflections to set database with fixtures. -* Remove deprecated `scope` use without passing a callable object. + Fixes #14845. - *Arun Agrawal* + *Kassio Borges* -* Remove deprecated `transaction_joinable=` in favor of `begin_transaction` - with `:joinable` option. +* Reset the cache when modifying a Relation with cached Arel. + Additionally display a warning message to make the user aware. - *Arun Agrawal* + *Yves Senn* -* Remove deprecated `decrement_open_transactions`. +* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures + different spellings of timestamps are treated the same. - *Arun Agrawal* + Example: -* Remove deprecated `increment_open_transactions`. + mytimestamp.simplified_type('timestamp without time zone') + # => :datetime + mytimestamp.simplified_type('timestamp(6) without time zone') + # => also :datetime (previously would be :timestamp) - *Arun Agrawal* + See #14513. -* Remove deprecated `PostgreSQLAdapter#outside_transaction?` - method. You can use `#transaction_open?` instead. + *Jefferson Lai* - *Yves Senn* +* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions. -* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of - `ActiveRecord::Fixtures.default_fixture_model_name`. + Fixes #14841. - *Vipul A M* + *Lucas Mazza* -* Removed deprecated `columns_for_remove` from `SchemaStatements`. +* Fix name collision with `Array#select!` with `Relation#select!`. - *Neeraj Singh* + Fixes #14752. -* Remove deprecated `SchemaStatements#distinct`. + *Earl St Sauver* - *Francesco Rodriguez* +* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`. -* Move deprecated `ActiveRecord::TestCase` into the rails test - suite. The class is no longer public and is only used for internal - Rails tests. + If a `has_many` association is adjusted using a scope, and another `has_many :through` + uses this association, then the scope adjustment is unexpectedly neglected. - *Yves Senn* + Fixes #14537. -* Removed support for deprecated option `:restrict` for `:dependent` - in associations. + *Jan Habermann* - *Neeraj Singh* +* `@destroyed` should always be set to `false` when an object is duped. -* Removed support for deprecated `delete_sql` in associations. + *Kuldeep Aggarwal* - *Neeraj Singh* +* Fixed has_many association to make it support irregular inflections. -* Removed support for deprecated `insert_sql` in associations. + Fixes #8928. - *Neeraj Singh* + *arthurnn*, *Javier Goizueta* -* Removed support for deprecated `finder_sql` in associations. +* Fixed a problem where count used with a grouping was not returning a Hash. - *Neeraj Singh* + Fixes #14721. -* Support array as root element in JSON fields. + *Eric Chahin* - *Alexey Noskov & Francesco Rodriguez* +* `sanitize_sql_like` helper method to escape a string for safe use in an SQL + LIKE statement. -* Removed support for deprecated `counter_sql` in associations. + Example: - *Neeraj Singh* + class Article + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term)) + end + end -* Do not invoke callbacks when `delete_all` is called on collection. + Article.search("20% _reduction_") + # => Query looks like "... title LIKE '20\% \_reduction\_' ..." - Method `delete_all` should not be invoking callbacks and this - feature was deprecated in Rails 4.0. This is being removed. - `delete_all` will continue to honor the `:dependent` option. However - if `:dependent` value is `:destroy` then the `:delete_all` deletion - strategy for that collection will be applied. + *Rob Gilson*, *Yves Senn* - User can also force a deletion strategy by passing parameter to - `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`. +* Do not quote uuid default value on `change_column`. - *Neeraj Singh* + Fixes #14604. -* Calling default_scope without a proc will now raise `ArgumentError`. + *Eric Chahin* - *Neeraj Singh* +* The comparison between `Relation` and `CollectionProxy` should be consistent. -* Removed deprecated method `type_cast_code` from Column. + Example: - *Neeraj Singh* + author.posts == Post.where(author_id: author.id) + # => true + Post.where(author_id: author.id) == author.posts + # => true -* Removed deprecated options `delete_sql` and `insert_sql` from HABTM - association. + Fixes #13506. - Removed deprecated options `finder_sql` and `counter_sql` from - collection association. + *Lauro Caetano* - *Neeraj Singh* +* Calling `delete_all` on an unloaded `CollectionProxy` no longer + generates an SQL statement containing each id of the collection: -* Remove deprecated `ActiveRecord::Base#connection` method. - Make sure to access it via the class. + Before: - *Yves Senn* + DELETE FROM `model` WHERE `model`.`parent_id` = 1 + AND `model`.`id` IN (1, 2, 3...) -* Remove deprecation warning for `auto_explain_threshold_in_seconds`. + After: - *Yves Senn* + DELETE FROM `model` WHERE `model`.`parent_id` = 1 -* Remove deprecated `:distinct` option from `Relation#count`. + *Eileen M. Uchitelle*, *Aaron Patterson* - *Yves Senn* +* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select` + which created invalid SQL. -* Removed deprecated methods `partial_updates`, `partial_updates?` and - `partial_updates=`. + Fixes #13648. - *Neeraj Singh* + *Simon Woker* -* Removed deprecated method `scoped`. +* PostgreSQL adapter only warns once for every missing OID per connection. - *Neeraj Singh* + Fixes #14275. -* Removed deprecated method `default_scopes?`. + *Matthew Draper*, *Yves Senn* - *Neeraj Singh* +* PostgreSQL adapter automatically reloads it's type map when encountering + unknown OIDs. -* Remove implicit join references that were deprecated in 4.0. + Fixes #14678. - Example: + *Matthew Draper*, *Yves Senn* - # before with implicit joins - Comment.where('posts.author_id' => 7) +* Fix insertion of records via `has_many :through` association with scope. - # after - Comment.references(:posts).where('posts.author_id' => 7) + Fixes #3548. - *Yves Senn* + *Ivan Antropov* -* Apply default scope when joining associations. For example: +* Auto-generate stable fixture UUIDs on PostgreSQL. - class Post < ActiveRecord::Base - default_scope -> { where published: true } - end + Fixes #11524. - class Comment - belongs_to :post - end + *Roderick van Domburg* - When calling `Comment.joins(:post)`, we expect to receive only - comments on published posts, since that is the default scope for - posts. +* Fixed a problem where an enum would overwrite values of another enum + with the same name in an unrelated class. - Before this change, the default scope from `Post` was not applied, - so we'd get comments on unpublished posts. + Fixes #14607. - *Jon Leighton* + *Evan Whalen* -* Remove `activerecord-deprecated_finders` as a dependency. +* PostgreSQL and SQLite string columns no longer have a default limit of 255. - *Łukasz Strzałkowski* + Fixes #13435, #9153. -* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0. + *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn* - *kennyj* +* Make possible to have an association called `records`. -* `find_each` now returns an `Enumerator` when called without a block, so that it - can be chained with other `Enumerable` methods. + Fixes #11645. - *Ben Woosley* + *prathamesh-sonpatki* -* `ActiveRecord::Result.each` now returns an `Enumerator` when called without - a block, so that it can be chained with other `Enumerable` methods. +* `to_sql` on an association now matches the query that is actually executed, where it + could previously have incorrectly accrued additional conditions (e.g. as a result of + a previous query). CollectionProxy now always defers to the association scope's + `arel` method so the (incorrect) inherited one should be entirely concealed. - *Ben Woosley* + Fixes #14003. -* Flatten merged join_values before building the joins. + *Jefferson Lai* - While joining_values special treatment is given to string values. - By flattening the array it ensures that string values are detected - as strings and not arrays. +* Block a few default Class methods as scope name. - Fixes #10669. + For instance, this will raise: - *Neeraj Singh and iwiznia* + scope :public, -> { where(status: 1) } -* Do not load all child records for inverse case. + *arthurnn* - currently `post.comments.find(Comment.first.id)` would load all - comments for the given post to set the inverse association. +* Fixed error when using `with_options` with lambda. - This has a huge performance penalty. Because if post has 100k - records and all these 100k records would be loaded in memory - even though the comment id was supplied. + Fixes #9805. - Fix is to use in-memory records only if loaded? is true. Otherwise - load the records using full sql. + *Lauro Caetano* - Fixes #10509. +* Switch `sqlite3:///` URLs (which were temporarily + deprecated in 4.1) from relative to absolute. - *Neeraj Singh* + If you still want the previous interpretation, you should replace + `sqlite3:///my/path` with `sqlite3:my/path`. -* `inspect` on Active Record model classes does not initiate a - new connection. This means that calling `inspect`, when the - database is missing, will no longer raise an exception. - Fixes #10936. + *Matthew Draper* - Example: +* Treat blank UUID values as `nil`. - Author.inspect # => "Author(no database connection)" + Example: - *Yves Senn* + Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil> -* Handle single quotes in PostgreSQL default column values. - Fixes #10881. + *Dmitry Lavrov* - *Dylan Markow* +* Enable support for materialized views on PostgreSQL >= 9.3. -* Log the sql that is actually sent to the database. + *Dave Lee* - If I have a query that produces sql - `WHERE "users"."name" = 'a b'` then in the log all the - whitespace is being squeezed. So the sql that is printed in the - log is `WHERE "users"."name" = 'a b'`. +* The PostgreSQL adapter supports custom domains. Fixes #14305. - Do not squeeze whitespace out of sql queries. Fixes #10982. + *Yves Senn* - *Neeraj Singh* +* PostgreSQL `Column#type` is now determined through the corresponding OID. + The column types stay the same except for enum columns. They no longer have + `nil` as type but `enum`. -* Fixture setup no longer depends on `ActiveRecord::Base.configurations`. - This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`. + See #7814. *Yves Senn* -* Fix mysql2 adapter raises the correct exception when executing a query on a - closed connection. +* Fixed error when specifying a non-empty default value on a PostgreSQL array column. - *Yves Senn* + Fixes #10613. -* Ambiguous reflections are on :through relationships are no longer supported. - For example, you need to change this: + *Luke Steensen* - class Author < ActiveRecord::Base - has_many :posts - has_many :taggings, through: :posts - end +* Fixed error where .persisted? throws SystemStackError for an unsaved model with a + custom primary key that didn't save due to validation error. - class Post < ActiveRecord::Base - has_one :tagging - has_many :taggings - end + Fixes #14393. - class Tagging < ActiveRecord::Base - end + *Chris Finne* - To this: +* Introduce `validate` as an alias for `valid?`. - class Author < ActiveRecord::Base - has_many :posts - has_many :taggings, through: :posts, source: :tagging - end + This is more intuitive when you want to run validations but don't care about the return value. - class Post < ActiveRecord::Base - has_one :tagging - has_many :taggings - end + *Henrik Nyh* - class Tagging < ActiveRecord::Base - end +* Create indexes inline in CREATE TABLE for MySQL. - *Aaron Patterson* + This is important, because adding an index on a temporary table after it has been created + would commit the transaction. -* Remove column restrictions for `count`, let the database raise if the SQL is - invalid. The previous behavior was untested and surprising for the user. - Fixes #5554. + It also allows creating and dropping indexed tables with fewer queries and fewer permissions + required. Example: - User.select("name, username").count - # Before => SELECT count(*) FROM users - # After => ActiveRecord::StatementInvalid - - # you can still use `count(:all)` to perform a query unrelated to the - # selected columns - User.select("name, username").count(:all) # => SELECT count(*) FROM users - - *Yves Senn* - -* Rails now automatically detects inverse associations. If you do not set the - `:inverse_of` option on the association, then Active Record will guess the - inverse association based on heuristics. - - Note that automatic inverse detection only works on `has_many`, `has_one`, - and `belongs_to` associations. Extra options on the associations will - also prevent the association's inverse from being found automatically. - - The automatic guessing of the inverse association uses a heuristic based - on the name of the class, so it may not work for all associations, - especially the ones with non-standard names. - - You can turn off the automatic detection of inverse associations by setting - the `:inverse_of` option to `false` like so: - - class Taggable < ActiveRecord::Base - belongs_to :tag, inverse_of: false + create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t| + t.index :zip end + # => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query + + *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca* - *John Wang* +* Use singular table name in generated migrations when + `ActiveRecord::Base.pluralize_table_names` is `false`. -* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432. + Fixes #13426. - *Adam Anderson* + *Kuldeep Aggarwal* -* Usage of `implicit_readonly` is being removed`. Please use `readonly` method - explicitly to mark records as `readonly. - Fixes #10615. +* `touch` accepts many attributes to be touched at once. Example: - user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first - user.todos_title = 'clean pet' - user.save! # will raise error + # touches :signed_at, :sealed_at, and :updated_at/on attributes. + Photo.last.touch(:signed_at, :sealed_at) - *Yves Senn* + *James Pinto* -* Fix the `:primary_key` option for `has_many` associations. +* `rake db:structure:dump` only dumps schema information if the schema + migration table exists. - Fixes #10693. + Fixes #14217. *Yves Senn* -* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1. - - Fixes #10620. +* Reap connections that were checked out by now-dead threads, instead + of waiting until they disconnect by themselves. Before this change, + a suitably constructed series of short-lived threads could starve + the connection pool, without ever having more than a couple alive at + the same time. - *Aaron Patterson* + *Matthew Draper* -* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1. +* `pk_and_sequence_for` now ensures that only the pg_depend entries + pointing to pg_class, and thus only sequence objects, are considered. - *kennyj* + *Josh Williams* -* Deprecate `ConnectionAdapters::SchemaStatements#distinct`, - as it is no longer used by internals. +* `where.not` adds `references` for `includes` like normal `where` calls do. - *Ben Woosley* + Fixes #14406. -* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix` - is not blank. + *Yves Senn* - Call `assume_migrated_upto_version` on connection to prevent it from first - being picked up in `method_missing`. +* Extend fixture `$LABEL` replacement to allow string interpolation. - In the base class, `Migration`, `method_missing` expects the argument to be a - table name, and calls `proper_table_name` on the arguments before sending to - `connection`. If `table_name_prefix` or `table_name_suffix` is used, the schema - version changes to `prefix_version_suffix`, breaking `rake test:prepare`. + Example: - Fixes #10411. + martin: + email: $LABEL@email.com - *Kyle Stevens* + users(:martin).email # => martin@email.com -* Method `read_attribute_before_type_cast` should accept input as symbol. + *Eric Steele* - *Neeraj Singh* +* Add support for `Relation` be passed as parameter on `QueryCache#select_all`. -* Confirm a record has not already been destroyed before decrementing counter cache. + Fixes #14361. - *Ben Tucker* + *arthurnn* -* Fixed a bug in `ActiveRecord#sanitize_sql_hash_for_conditions` in which - `self.class` is an argument to `PredicateBuilder#build_from_hash` - causing `PredicateBuilder` to call non-existent method - `Class#reflect_on_association`. +* Passing an Active Record object to `find` or `exists?` is now deprecated. + Call `.id` on the object first. - *Zach Ohlgren* + *Aaron Patterson* -* While removing index if column option is missing then raise IrreversibleMigration exception. +* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. - Following code should raise `IrreversibleMigration`. But the code was - failing since options is an array and not a hash. + *Ryuta Kamizono* - def change - change_table :users do |t| - t.remove_index [:name, :email] - end - end +* Support for MySQL 5.6 fractional seconds. - Fix was to check if the options is a Hash before operating on it. + *arthurnn*, *Tatsuhiko Miyagawa* - Fixes #10419. +* Support for Postgres `citext` data type enabling case-insensitive where + values without needing to wrap in UPPER/LOWER sql functions. - *Neeraj Singh* + *Troy Kruthoff*, *Lachlan Sylvester* -* Do not overwrite manually built records during one-to-one nested attribute assignment +* Only save has_one associations if record has changes. + Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one + object did not get saved to the db. - For one-to-one nested associations, if you build the new (in-memory) - child object yourself before assignment, then the NestedAttributes - module will not overwrite it, e.g.: + *Alan Kennedy* - class Member < ActiveRecord::Base - has_one :avatar - accepts_nested_attributes_for :avatar +* Allow strings to specify the `#order` value. - def avatar - super || build_avatar(width: 200) - end - end + Example: - member = Member.new - member.avatar_attributes = {icon: 'sad'} - member.avatar.width # => 200 + Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql - *Olek Janiszewski* + *Marcelo Casiraghi*, *Robin Dupret* -* fixes bug introduced by #3329. Now, when autosaving associations, - deletions happen before inserts and saves. This prevents a 'duplicate - unique value' database error that would occur if a record being created had - the same value on a unique indexed field as that of a record being destroyed. +* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID" + warnings on enum columns. - *Johnny Holton* + *Dieter Komendera* -* Handle aliased attributes in ActiveRecord::Relation. +* `includes` is able to detect the right preloading strategy when string + joins are involved. - When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database: + Fixes #14109. - With the model + *Aaron Patterson*, *Yves Senn* - class Topic - alias_attribute :heading, :title - end +* Fixed error with validation with enum fields for records where the + value for any enum attribute is always evaluated as 0 during + uniqueness validation. - The call + Fixes #14172. - Topic.where(heading: 'The First Topic') + *Vilius Luneckas* *Ahmed AbouElhamayed* - should yield the same result as +* `before_add` callbacks are fired before the record is saved on + `has_and_belongs_to_many` associations *and* on `has_many :through` + associations. Before this change, `before_add` callbacks would be fired + before the record was saved on `has_and_belongs_to_many` associations, but + *not* on `has_many :through` associations. - Topic.where(title: 'The First Topic') + Fixes #14144. - This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`. +* Fixed STI classes not defining an attribute method if there is a + conflicting private method defined on its ancestors. - This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`. + Fixes #11569. *Godfrey Chan* -* Mute `psql` output when running rake db:schema:load. +* Coerce strings when reading attributes. Fixes #10485. - *Godfrey Chan* + Example: -* Trigger a save on `has_one association=(associate)` when the associate contents have changed. + book = Book.new(title: 12345) + book.save! + book.title # => "12345" - Fixes #8856. + *Yves Senn* - *Chris Thompson* +* Deprecate half-baked support for PostgreSQL range values with excluding beginnings. + We currently map PostgreSQL ranges to Ruby ranges. This conversion is not fully + possible because the Ruby range does not support excluded beginnings. -* Abort a rake task when missing db/structure.sql like `db:schema:load` task. + The current solution of incrementing the beginning is not correct and is now + deprecated. For subtypes where we don't know how to increment (e.g. `#succ` + is not defined) it will raise an ArgumentException for ranges with excluding + beginnings. - *kennyj* + *Yves Senn* -* rake:db:test:prepare falls back to original environment after execution. +* Support for user created range types in PostgreSQL. - *Slava Markevich* + *Yves Senn* -Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index e04abe9b37..f4777919d3 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -1,4 +1,4 @@ -= Active Record -- Object-relational mapping put on rails += Active Record -- Object-relational mapping in Rails Active Record connects classes to relational database tables to establish an almost zero-configuration persistence layer for applications. The library @@ -20,8 +20,10 @@ A short rundown of some of the major features: class Product < ActiveRecord::Base end - The Product class is automatically mapped to the table named "products", - which might look like this: + {Learn more}[link:classes/ActiveRecord/Base.html] + +The Product class is automatically mapped to the table named "products", +which might look like this: CREATE TABLE products ( id int(11) NOT NULL auto_increment, @@ -29,10 +31,8 @@ A short rundown of some of the major features: PRIMARY KEY (id) ); - This would also define the following accessors: `Product#name` and - `Product#name=(new_name)` - - {Learn more}[link:classes/ActiveRecord/Base.html] +This would also define the following accessors: `Product#name` and +`Product#name=(new_name)`. * Associations between objects defined by simple class methods. @@ -130,7 +130,7 @@ A short rundown of some of the major features: SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. -* Logging support for Log4r[http://log4r.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. +* Logging support for Log4r[https://github.com/colbygk/log4r] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.logger = Log4r::Logger.new('Application Log') @@ -208,6 +208,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 6f8948f987..7769966a22 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -38,33 +38,36 @@ namespace :test do end end +desc 'Build MySQL and PostgreSQL test databases' namespace :db do - desc 'Build MySQL and PostgreSQL test databases' - task create: ['mysql:build_databases', 'postgresql:build_databases'] - desc 'Drop MySQL and PostgreSQL test databases' - task drop: ['mysql:drop_databases', 'postgresql:drop_databases'] + task :create => ['db:mysql:build', 'db:postgresql:build'] + task :drop => ['db:mysql:drop', 'db:postgresql:drop'] end -%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| - Rake::TestTask.new("test_#{adapter}") { |t| - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] - t.libs << 'test' - t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { - |x| x =~ /\/adapters\// - } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort - - t.warning = true - t.verbose = true - } - - task "isolated_test_#{adapter}" do - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] - puts [adapter, adapter_short].inspect - (Dir["test/cases/**/*_test.rb"].reject { - |x| x =~ /\/adapters\// - } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(Gem.ruby, '-w' ,"-Itest", file) - end or raise "Failures" +%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| + namespace :test do + Rake::TestTask.new(adapter => "#{adapter}:env") { |t| + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] + t.libs << 'test' + t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { + |x| x =~ /\/adapters\// + } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort + + t.warning = true + t.verbose = true + } + + namespace :isolated do + task adapter => "#{adapter}:env" do + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] + puts [adapter, adapter_short].inspect + (Dir["test/cases/**/*_test.rb"].reject { + |x| x =~ /\/adapters\// + } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| + sh(Gem.ruby, '-w' ,"-Itest", file) + end or raise "Failures" + end + end end namespace adapter do @@ -76,8 +79,8 @@ end end # Make sure the adapter test evaluates the env setting task - task "test_#{adapter}" => "#{adapter}:env" - task "isolated_test_#{adapter}" => "#{adapter}:env" + task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"] + task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] end rule '.sqlite3' do |t| @@ -89,109 +92,58 @@ task :test_sqlite3 => [ 'test/fixtures/fixture_database_2.sqlite3' ] -namespace :mysql do - desc 'Build the MySQL test databases' - task :build_databases do - config = ARTest.config['connections']['mysql'] - %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - end - - desc 'Drop the MySQL test databases' - task :drop_databases do - config = ARTest.config['connections']['mysql'] - %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) - %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) - end - - desc 'Rebuild the MySQL test databases' - task :rebuild_databases => [:drop_databases, :build_databases] -end - -task :build_mysql_databases => 'mysql:build_databases' -task :drop_mysql_databases => 'mysql:drop_databases' -task :rebuild_mysql_databases => 'mysql:rebuild_databases' - - -namespace :postgresql do - desc 'Build the PostgreSQL test databases' - task :build_databases do - config = ARTest.config['connections']['postgresql'] - %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} ) - %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} ) +namespace :db do + namespace :mysql do + desc 'Build the MySQL test databases' + task :build do + config = ARTest.config['connections']['mysql'] + %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + end - # notify about preparing hstore - if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + desc 'Drop the MySQL test databases' + task :drop do + config = ARTest.config['connections']['mysql'] + %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) + %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) end - end - desc 'Drop the PostgreSQL test databases' - task :drop_databases do - config = ARTest.config['connections']['postgresql'] - %x( dropdb #{config['arunit']['database']} ) - %x( dropdb #{config['arunit2']['database']} ) + desc 'Rebuild the MySQL test databases' + task :rebuild => [:drop, :build] end - desc 'Rebuild the PostgreSQL test databases' - task :rebuild_databases => [:drop_databases, :build_databases] -end - -task :build_postgresql_databases => 'postgresql:build_databases' -task :drop_postgresql_databases => 'postgresql:drop_databases' -task :rebuild_postgresql_databases => 'postgresql:rebuild_databases' - - -namespace :frontbase do - desc 'Build the FrontBase test databases' - task :build_databases => :rebuild_frontbase_databases - - desc 'Rebuild the FrontBase test databases' - task :rebuild_databases do - build_frontbase_database = Proc.new do |db_name, sql_definition_file| - %( - STOP DATABASE #{db_name}; - DELETE DATABASE #{db_name}; - CREATE DATABASE #{db_name}; + namespace :postgresql do + desc 'Build the PostgreSQL test databases' + task :build do + config = ARTest.config['connections']['postgresql'] + %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} ) + %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} ) - CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM; - SET COMMIT FALSE; - - CREATE USER RAILS; - CREATE SCHEMA RAILS AUTHORIZATION RAILS; - COMMIT; - - SET SESSION AUTHORIZATION RAILS; - SCRIPT '#{sql_definition_file}'; - - COMMIT; - - DISCONNECT ALL; - ) + # prepare hstore + if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + end end - config = ARTest.config['connections']['frontbase'] - create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')] - create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')] - execute_frontbase_sql = Proc.new do |sql| - system(<<-SHELL) - /Library/FrontBase/bin/sql92 <<-SQL - #{sql} - SQL - SHELL + + desc 'Drop the PostgreSQL test databases' + task :drop do + config = ARTest.config['connections']['postgresql'] + %x( dropdb #{config['arunit']['database']} ) + %x( dropdb #{config['arunit2']['database']} ) end - execute_frontbase_sql[create_activerecord_unittest] - execute_frontbase_sql[create_activerecord_unittest2] + + desc 'Rebuild the PostgreSQL test databases' + task :rebuild => [:drop, :build] end end -task :build_frontbase_databases => 'frontbase:build_databases' -task :rebuild_frontbase_databases => 'frontbase:rebuild_databases' +task :build_mysql_databases => 'db:mysql:build' +task :drop_mysql_databases => 'db:mysql:drop' +task :rebuild_mysql_databases => 'db:mysql:rebuild' -spec = eval(File.read('activerecord.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end +task :build_postgresql_databases => 'db:postgresql:build' +task :drop_postgresql_databases => 'db:postgresql:drop' +task :rebuild_postgresql_databases => 'db:postgresql:rebuild' task :lines do lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 @@ -217,6 +169,11 @@ task :lines do puts "Total: Lines #{total_lines}, LOC #{total_codelines}" end +spec = eval(File.read('activerecord.gemspec')) + +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end # Publishing ------------------------------------------------------ diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index d397c9e016..8075008574 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 5.0.0' + s.add_dependency 'arel', '~> 6.0.0' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f856c482d6..9028970a3d 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -27,10 +27,12 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload + autoload :Attribute autoload :Base autoload :Callbacks autoload :Core @@ -95,6 +97,7 @@ module ActiveRecord module Coders autoload :YAMLColumn, 'active_record/coders/yaml_column' + autoload :JSON, 'active_record/coders/json' end module AttributeMethods diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0d5313956b..e576ec4d40 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -129,10 +129,10 @@ module ActiveRecord # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that - # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor - # for the value class is called +create+ and it expects a CIDR address string as a parameter. New - # values can be assigned to the value object using either another NetAddr::CIDR object, a string + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). + # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. + # New values can be assigned to the value object using either another NetAddr::CIDR object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet # these requirements: # @@ -230,8 +230,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) @@ -244,15 +244,19 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| klass = class_name.constantize + if part.is_a?(Hash) + part = klass.new(*part.values) + end + unless part.is_a?(klass) || converter.nil? || part.nil? part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) end if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } + mapping.each { |key, _| self[key] = nil } @aggregation_cache[name] = nil else - mapping.each { |pair| self[pair.first] = part.send(pair.last) } + mapping.each { |key, value| self[key] = part.send(value) } @aggregation_cache[name] = part.freeze end end diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 20516bba0c..5a84792f45 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -9,6 +9,10 @@ module ActiveRecord @association end + def ==(other) + other == to_a + end + private def exec_queries diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 142d21ce92..830c633c36 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -4,6 +4,12 @@ require 'active_support/core_ext/module/remove_method' require 'active_record/errors' module ActiveRecord + class AssociationNotFoundError < ConfigurationError #:nodoc: + def initialize(record, association_name) + super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + end + end + class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection, associated_class = nil) super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") @@ -44,7 +50,7 @@ module ActiveRecord def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } + source_associations = reflection.through_reflection.klass._reflections.keys super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -145,7 +151,7 @@ module ActiveRecord association = association_instance_get(name) if association.nil? - reflection = self.class.reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -196,12 +202,13 @@ module ActiveRecord # For instance, +attributes+ and +connection+ would be bad choices for association names. # # == Auto-generated methods + # See also Instance Public methods below for more details. # # === Singular associations (one-to-one) # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other | X | X | X + # other(force_reload=false) | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -211,7 +218,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others | X | X | X + # others(force_reload=false) | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -413,6 +420,10 @@ module ActiveRecord # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' # end # + # Note: Joining, eager loading and preloading of these associations is not fully possible. + # These operations happen before instance creation and the scope will be called with a +nil+ argument. + # This can lead to unexpected behavior and is deprecated. + # # == Association callbacks # # Similar to the normal callbacks that hook into the life cycle of an Active Record object, @@ -530,8 +541,8 @@ module ActiveRecord # end # # @firm = Firm.first - # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model + # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model # # Similarly you can go through a +has_one+ association on the join model: # @@ -706,9 +717,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -767,11 +778,16 @@ module ActiveRecord # like this can have unintended consequences. # In the above example posts with no approved comments are not returned at all, because # the conditions apply to the SQL statement as a whole and not just to the association. + # # You must disambiguate column references for this fallback to happen, for example # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. # - # If you do want eager load only some members of an association it is usually more natural - # to include an association which has conditions defined on it: + # If you want to load all posts (including posts with no approved comments) then write + # your own LEFT OUTER JOIN query using ON + # + # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") + # + # In this case it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' @@ -1036,6 +1052,9 @@ module ActiveRecord # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: # + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. + # # [collection(force_reload = false)] # Returns an array of all the associated objects. # An empty array is returned if none are found. @@ -1094,9 +1113,6 @@ module ActiveRecord # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> # if the record is invalid. # - # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so - # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.) - # # === Example # # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add: @@ -1115,7 +1131,7 @@ module ActiveRecord # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Options # [:class_name] @@ -1211,6 +1227,9 @@ module ActiveRecord # # The following methods for retrieval and query of a single associated object will be added: # + # +association+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. + # # [association(force_reload = false)] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] @@ -1229,9 +1248,6 @@ module ActiveRecord # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> # if the record is invalid. # - # (+association+ is replaced with the symbol passed as the first argument, so - # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.) - # # === Example # # An Account class declares <tt>has_one :beneficiary</tt>, which will add: @@ -1243,7 +1259,7 @@ module ActiveRecord # # === Options # - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # Options are: # [:class_name] @@ -1293,6 +1309,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1304,6 +1324,7 @@ module ActiveRecord # has_one :boss, readonly: :true # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1317,6 +1338,9 @@ module ActiveRecord # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: # + # +association+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. + # # [association(force_reload = false)] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] @@ -1332,9 +1356,6 @@ module ActiveRecord # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> # if the record is invalid. # - # (+association+ is replaced with the symbol passed as the first argument, so - # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.) - # # === Example # # A Post class declares <tt>belongs_to :author</tt>, which will add: @@ -1343,7 +1364,7 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Options # @@ -1397,7 +1418,7 @@ module ActiveRecord # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) + # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] @@ -1405,18 +1426,23 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" # belongs_to :person, primary_key: "name", foreign_key: "person_name" # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true # belongs_to :project, readonly: true # belongs_to :post, counter_cache: true # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :company, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1454,6 +1480,9 @@ module ActiveRecord # # Adds the following methods for retrieval and query: # + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. + # # [collection(force_reload = false)] # Returns an array of all the associated objects. # An empty array is returned if none are found. @@ -1495,9 +1524,6 @@ module ActiveRecord # with +attributes+, linked to this object through the join table, and that has already been # saved (if it passed the validation). # - # (+collection+ is replaced with the symbol passed as the first argument, so - # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.) - # # === Example # # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add: @@ -1515,7 +1541,7 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) - # The declaration may include an options hash to specialize the behavior of the association. + # The declaration may include an +options+ hash to specialize the behavior of the association. # # === Options # @@ -1561,14 +1587,22 @@ module ActiveRecord scope = nil end + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) + builder = Builder::HasAndBelongsToMany.new name, self, options join_model = builder.through_model + # FIXME: we should move this to the internal constants. Also people + # should never directly access this constant so I'm not happy about + # setting it. + const_set join_model.name, join_model + middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = [name.to_s, habtm_reflection] include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1584,11 +1618,12 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k| hm_options[k] = options[k] if options.key? k end has_many name, scope, hm_options, &extension + self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 85109aee6c..a6a1947148 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -32,8 +32,18 @@ module ActiveRecord join.left.downcase.scan( /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ ).size - else + elsif join.respond_to? :left join.left.table_name == name ? 1 : 0 + else + # this branch is reached by two tests: + # + # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 + # with :posts + # + # activerecord/test/cases/associations/eager_test.rb:1133 + # with :comments + # + 0 end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 4e46256862..f1c36cd047 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -160,7 +160,7 @@ module ActiveRecord def marshal_load(data) reflection_name, ivars = data ivars.each { |name, val| instance_variable_set(name, val) } - @reflection = @owner.class.reflect_on_association(reflection_name) + @reflection = @owner.class._reflect_on_association(reflection_name) end def initialize_attributes(record) #:nodoc: @@ -179,7 +179,7 @@ module ActiveRecord def creation_attributes attributes = {} - if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] + if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] @@ -232,7 +232,7 @@ module ActiveRecord # Returns true if record contains the foreign_key def foreign_key_for?(record) - record.attributes.has_key? reflection.foreign_key + record.has_attribute?(reflection.foreign_key) end # This should be implemented to return the values of the relevant key(s) on the owner, diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 27fd9e35db..519d4d8651 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,12 +1,34 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - INSTANCE = new - def self.scope(association, connection) INSTANCE.scope association, connection end + class BindSubstitution + def initialize(block) + @block = block + end + + def bind_value(scope, column, value, alias_tracker) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, @block.call(value)]] + substitute + end + end + + def self.create(&block) + block = block ? block : lambda { |val| val } + new BindSubstitution.new(block) + end + + def initialize(bind_substitution) + @bind_substitution = bind_substitution + end + + INSTANCE = create + def scope(association, connection) klass = association.klass reflection = association.reflection @@ -22,6 +44,23 @@ module ActiveRecord Arel::Nodes::InnerJoin end + def self.get_bind_values(owner, chain) + bvs = [] + chain.each_with_index do |reflection, i| + if reflection == chain.last + bvs << reflection.join_id_for(owner) + if reflection.type + bvs << owner.class.base_class.name + end + else + if reflection.type + bvs << chain[i + 1].klass.base_class.name + end + end + end + bvs + end + private def construct_tables(chain, klass, refl, alias_tracker) @@ -49,10 +88,7 @@ module ActiveRecord end def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - scope.bind_values += [[column, value]] - substitute + @bind_substitution.bind_value scope, column, value, alias_tracker end def bind(scope, table_name, column_name, value, tracker) @@ -69,18 +105,9 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :belongs_to - if reflection.options[:polymorphic] - key = reflection.association_primary_key(assoc_klass) - else - key = reflection.association_primary_key - end - - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker @@ -120,6 +147,7 @@ module ActiveRecord end scope.where_values += item.where_values + scope.bind_values += item.bind_values scope.order_values |= item.order_values end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8272a5584c..81fdd681de 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -31,6 +31,14 @@ module ActiveRecord @updated end + def decrement_counters # :nodoc: + with_cache_name { |name| decrement_counter name } + end + + def increment_counters # :nodoc: + with_cache_name { |name| increment_counter name } + end + private def find_target? @@ -51,13 +59,15 @@ module ActiveRecord end end - def decrement_counters - with_cache_name { |name| decrement_counter name } + def decrement_counter(counter_cache_name) + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) + end end - def decrement_counter counter_cache_name + def increment_counter(counter_cache_name) if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) + klass.increment_counter(counter_cache_name, target_id) end end @@ -82,7 +92,7 @@ module ActiveRecord # has_one associations. def invertible_for?(record) inverse = inverse_reflection_for(record) - inverse && inverse.macro == :has_one + inverse && inverse.has_one? end def target_id diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..947d61ee7b 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + define_validations model, reflection builder.define_extensions model reflection end @@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end private - def self.add_before_destroy_callbacks(model, reflection) - unless valid_dependent_options.include? reflection.options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 5ccaa55a32..954ea3878a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -26,28 +26,9 @@ module ActiveRecord::Associations::Builder private def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_create + return if mixin.method_defined? :belongs_to_counter_cache_after_update mixin.class_eval do - def belongs_to_counter_cache_after_create(reflection) - if record = send(reflection.name) - cache_column = reflection.counter_cache_column - record.class.increment_counter(cache_column, record.id) - @_after_create_counter_called = true - end - end - - def belongs_to_counter_cache_before_destroy(reflection) - foreign_key = reflection.foreign_key.to_sym - unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - record = send reflection.name - if record && !self.destroyed? - cache_column = reflection.counter_cache_column - record.class.decrement_counter(cache_column, record.id) - end - end - end - def belongs_to_counter_cache_after_update(reflection) foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column @@ -73,14 +54,6 @@ module ActiveRecord::Associations::Builder def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - model.after_create lambda { |record| - record.belongs_to_counter_cache_after_create(reflection) - } - - model.before_destroy lambda { |record| - record.belongs_to_counter_cache_before_destroy(reflection) - } - model.after_update lambda { |record| record.belongs_to_counter_cache_after_update(reflection) } @@ -130,9 +103,14 @@ module ActiveRecord::Associations::Builder BelongsTo.touch_record(record, foreign_key, n, touch) } - model.after_save callback + model.after_save callback, if: :changed? model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + end 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 e472277374..34a555dfd4 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 @@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") end private - def klass; @rhs_class_name.constantize; end + + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -60,13 +63,13 @@ module ActiveRecord::Associations::Builder def self.add_left_association(name, options) belongs_to name, options - self.left_reflection = reflect_on_association(name) + self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym belongs_to rhs_name, options - self.right_reflection = reflect_on_association(rhs_name) + self.right_reflection = _reflect_on_association(rhs_name) end } diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7909b93622..4c8c826f76 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index f359efd496..c194c8ae9a 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - valid = super + [:order, :as] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder private - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] 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 e655c389a6..6e6dd7204c 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: def valid_options - super + [:remote, :dependent, :primary_key, :inverse_of] + super + [:dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name + end + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 03ca00fa70..065a2cff01 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -24,10 +24,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - def initialize(owner, reflection) - super - @proxy = CollectionProxy.create(klass, self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -37,7 +33,7 @@ module ActiveRecord reload end - @proxy + @proxy ||= CollectionProxy.create(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -59,9 +55,9 @@ module ActiveRecord # 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 + pk_type = reflection.primary_key_type ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast(i) } + ids.map! { |i| pk_type.type_cast_from_user(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end @@ -138,20 +134,19 @@ module ActiveRecord end def create(attributes = {}, &block) - create_record(attributes, &block) + _create_record(attributes, &block) end def create!(attributes = {}, &block) - create_record(attributes, true, &block) + _create_record(attributes, true, &block) end # Add +records+ to this association. Returns +self+ so method calls may # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. def concat(*records) - load_target if owner.new_record? - if owner.new_record? + load_target concat_records(records) else transaction { concat_records(records) } @@ -187,11 +182,11 @@ module ActiveRecord # # See delete for more info. def delete_all(dependent = nil) - if dependent.present? && ![:nullify, :delete_all].include?(dependent) + if dependent && ![:nullify, :delete_all].include?(dependent) raise ArgumentError, "Valid values are :nullify or :delete_all" end - dependent = if dependent.present? + dependent = if dependent dependent elsif options[:dependent] == :destroy :delete_all @@ -199,7 +194,7 @@ module ActiveRecord options[:dependent] end - delete(:all, dependent: dependent).tap do + delete_or_nullify_all_records(dependent).tap do reset loaded! end @@ -249,19 +244,12 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) + return if records.empty? _options = records.extract_options! dependent = _options[:dependent] || options[:dependent] - if records.first == :all - if loaded? || dependent == :destroy - delete_or_destroy(load_target, dependent) - else - delete_records(:all, dependent) - end - else - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } - delete_or_destroy(records, dependent) - end + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, dependent) end # Deletes the +records+ and removes them from this association calling @@ -270,6 +258,7 @@ module ActiveRecord # Note that this method removes records from the database ignoring the # +:dependent+ option. def destroy(*records) + return if records.empty? records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end @@ -363,7 +352,9 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else - transaction { replace_records(other_array, original_target) } + if other_array != original_target + transaction { replace_records(other_array, original_target) } + end end end @@ -372,7 +363,7 @@ module ActiveRecord if record.new_record? include_in_memory?(record) else - loaded? ? target.include?(record) : scope.exists?(record) + loaded? ? target.include?(record) : scope.exists?(record.id) end else false @@ -415,9 +406,23 @@ module ActiveRecord end private + def get_records + return scope.to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end def find_target - records = scope.to_a + records = get_records records.each { |record| set_inverse_instance(record) } records end @@ -452,13 +457,13 @@ module ActiveRecord persisted + memory end - def create_record(attributes, raise = false, &block) + def _create_record(attributes, raise = false, &block) unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end if attributes.is_a?(Array) - attributes.collect { |attr| create_record(attr, raise, &block) } + attributes.collect { |attr| _create_record(attr, raise, &block) } else transaction do add_to_target(build_record(attributes)) do |record| @@ -517,13 +522,13 @@ module ActiveRecord target end - def concat_records(records) + def concat_records(records, should_raise = false) result = true records.flatten.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do |rec| - result &&= insert_record(rec) unless owner.new_record? + result &&= insert_record(rec, true, should_raise) unless owner.new_record? end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index eba688866c..84c8cfe72b 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -357,7 +357,7 @@ module ActiveRecord # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the <tt>:dependent</tt> - # option. Returns an array with the deleted records. + # option. # # If no <tt>:dependent</tt> option is given, then it will follow the # default strategy. The default strategy is <tt>:nullify</tt>. This @@ -435,11 +435,6 @@ module ActiveRecord # # ] # # person.pets.delete_all - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound @@ -860,6 +855,10 @@ module ActiveRecord !!@association.include?(record) end + def arel + scope.arel + end + def proxy_association @association end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 6457182195..79c3d2b0f5 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -41,6 +41,14 @@ module ActiveRecord end end + def empty? + if has_cached_counter? + size.zero? + else + super + end + end + private # Returns the number of records in this collection. @@ -58,7 +66,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - owner.send(:read_attribute, cached_counter_attribute_name) + owner.read_attribute cached_counter_attribute_name else scope.count end @@ -71,18 +79,29 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - def has_cached_counter?(reflection = reflection) + def has_cached_counter?(reflection = reflection()) owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def cached_counter_attribute_name(reflection = reflection) + def cached_counter_attribute_name(reflection = reflection()) options[:counter_cache] || "#{reflection.name}_count" end - def update_counter(difference, reflection = reflection) + def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) + end + end + + def update_counter_in_memory(difference, reflection = reflection()) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) owner[counter] += difference owner.changed_attributes.delete(counter) # eww end @@ -98,30 +117,39 @@ module ActiveRecord # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = reflection) + def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + inverse_updates_counter_named?(counter_name, reflection) + end + + def inverse_updates_counter_named?(counter_name, reflection = reflection()) + reflection.klass._reflections.values.any? { |inverse_reflection| + inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end + def delete_count(method, scope) + if method == :delete_all + scope.delete_all + else + scope.update_all(reflection.foreign_key => nil) + end + end + + def delete_or_nullify_all_records(method) + count = delete_count(method, self.scope) + update_counter(-count) + end + # Deletes the records according to the <tt>:dependent</tt> option. def delete_records(records, method) if method == :destroy records.each(&:destroy!) update_counter(-records.length) unless inverse_updates_counter_cache? else - if records == :all || !reflection.klass.primary_key - scope = self.scope - else - scope = self.scope.where(reflection.klass.primary_key => records) - end - - if method == :delete_all - update_counter(-scope.delete_all) - else - update_counter(-scope.update_all(reflection.foreign_key => nil)) - end + scope = self.scope.where(reflection.klass.primary_key => records) + update_counter(-delete_count(method, scope)) end end @@ -132,6 +160,25 @@ module ActiveRecord false end end + + def concat_records(records, *) + update_counter_if_success(super, records.length) + end + + def _create_record(attributes, *) + if attributes.is_a?(Array) + super + else + update_counter_if_success(super, 1) + end + end + + def update_counter_if_success(saved_successfully, difference) + if saved_successfully + update_counter_in_memory(difference) + end + saved_successfully + 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 31b8d27892..44c4436e95 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Has Many Through Association module Associations @@ -12,17 +11,18 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been - # loaded and calling collection.size if it has. If it's more likely than not that the collection does - # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer - # SELECT query if you use #length. + # Returns the size of the collection by executing a SELECT COUNT(*) query + # if the collection hasn't been loaded, and by calling collection.size if + # it has. If the collection will likely have a size greater than zero, + # and if fetching the collection will be needed afterwards, one less + # SELECT query will be generated by using #length instead. def size if has_cached_counter? - owner.send(:read_attribute, cached_counter_attribute_name) + owner.read_attribute cached_counter_attribute_name(reflection) elsif loaded? target.size else - count + super end end @@ -30,7 +30,6 @@ module ActiveRecord unless owner.new_record? records.flatten.each do |record| raise_on_type_mismatch!(record) - record.save! if record.new_record? end end @@ -40,7 +39,7 @@ module ActiveRecord def concat_records(records) ensure_not_nested - records = super + records = super(records, true) if owner.new_record? && records records.flatten.each do |record| @@ -63,7 +62,15 @@ module ActiveRecord end save_through_record(record) - update_counter(1) + if has_cached_counter? && !through_reflection_updates_counter_cache? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Automatic updating of counter caches on through associations has been + deprecated, and will be removed in Rails 5.0. Instead, please set the + appropriate counter_cache options on the has_many and belongs_to for + your associations to #{through_reflection.name}. + MESSAGE + update_counter_in_database(1) + end record end @@ -73,23 +80,31 @@ module ActiveRecord @through_association ||= owner.association(through_reflection.name) end - # We temporarily cache through record that has been build, because if we build a - # through record in build_record and then subsequently call insert_record, then we - # want to use the exact same object. + # The through record (built with build_record) is temporarily cached + # so that it may be reused if insert_record is subsequently called. # - # However, after insert_record has been called, we clear the cache entry because - # we want it to be possible to have multiple instances of the same record in an - # association + # However, after insert_record has been called, the cache is cleared in + # order to allow multiple instances of the same record in an association. def build_through_record(record) @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build + through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) through_record end end + def options_for_through_record + [through_scope_attributes] + end + + def through_scope_attributes + scope.where_values_hash(through_association.reflection.name.to_s). + except!(through_association.reflection.foreign_key, + through_association.reflection.klass.inheritance_column) + end + def save_through_record(record) build_through_record(record).save! ensure @@ -103,9 +118,9 @@ module ActiveRecord inverse = source_reflection.inverse_of if inverse - if inverse.macro == :has_many + if inverse.collection? record.send(inverse.name) << build_through_record(record) - elsif inverse.macro == :has_one + elsif inverse.has_one? record.send("#{inverse.name}=", build_through_record(record)) end end @@ -114,7 +129,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) + !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) @@ -128,13 +143,13 @@ module ActiveRecord end end + def delete_or_nullify_all_records(method) + delete_records(load_target, method) + end + def delete_records(records, method) ensure_not_nested - # This is unoptimised; it will load all the target records - # even when we just want to delete everything. - records = load_target if records == :all - scope = through_association.scope scope.where! construct_join_attributes(*records) @@ -168,7 +183,7 @@ module ActiveRecord klass.decrement_counter counter, records.map(&:id) end - if through_reflection.macro == :has_many && update_through_counter?(method) + if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) end @@ -178,14 +193,18 @@ module ActiveRecord def through_records_for(record) attributes = construct_join_attributes(record) candidates = Array.wrap(through_association.target) - candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes } + candidates.find_all do |c| + attributes.all? do |key, value| + c.public_send(key) == value + end + end end def delete_through_records(records) records.each do |record| through_records = through_records_for(record) - if through_reflection.macro == :has_many + if through_reflection.collection? through_records.each { |r| through_association.target.delete(r) } else if through_records.include?(through_association.target) @@ -199,13 +218,18 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scope.to_a + get_records end # NOTE - not sure that we can actually cope with inverses here def invertible_for?(record) false end + + def through_reflection_updates_counter_cache? + counter_name = cached_counter_attribute_name + inverse_updates_counter_named?(counter_name, through_reflection) + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 944caacab6..e6095d84dc 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Belongs To Has One Association module Associations diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 94f69d4c2d..ec5c189cd3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -131,7 +131,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -144,8 +143,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } @@ -164,17 +162,17 @@ module ActiveRecord def make_outer_joins(parent, child) tables = table_aliases_for(parent, child) join_type = Arel::Nodes::OuterJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } + [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_inner_joins(child, c) } end def table_aliases_for(parent, node) @@ -207,7 +205,7 @@ module ActiveRecord end def find_reflection(klass, name) - klass.reflect_on_association(name) or + klass._reflect_on_association(name) or raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end @@ -215,8 +213,9 @@ module ActiveRecord associations.map do |name, right| reflection = find_reflection base_klass, name reflection.check_validity! + reflection.check_eager_loadable! - if reflection.options[:polymorphic] + if reflection.polymorphic? raise EagerLoadPolymorphicError.new(reflection) end 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 cee3c9999f..c3bbdccad8 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -21,8 +21,11 @@ module ActiveRecord super && reflection == other.reflection end + JoinInformation = Struct.new :joins, :binds + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] + bind_values = [] tables = tables.reverse scope_chain_index = 0 @@ -34,14 +37,9 @@ module ActiveRecord table = tables.shift klass = reflection.klass - case reflection.source_macro - when :belongs_to - key = reflection.association_primary_key - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(klass) + key = join_keys.key + foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) @@ -54,27 +52,33 @@ module ActiveRecord end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope)].compact + scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right end - if reflection.type - constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name - end - if rel && !rel.arel.constraints.empty? + bind_values.concat rel.bind_values constraint = constraint.and rel.arel.constraints end + if reflection.type + value = foreign_klass.base_class.name + column = klass.columns_hash[column.to_s] + + substitute = klass.connection.substitute_at(column, bind_values.length) + bind_values.push [column, value] + constraint = constraint.and table[reflection.type].eq substitute + end + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end - joins + JoinInformation.new joins, bind_values end # Builds equality condition. @@ -86,7 +90,7 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # klass # => Physician # table # => #<Arel::Table @name="appointments" ...> # key # => physician_id # foreign_table # => #<Arel::Table @name="physicians" ...> diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 83637a0409..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -80,7 +80,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values).new({}) + NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq @@ -112,13 +112,14 @@ module ActiveRecord end def preloaders_for_hash(association, records, scope) - parent, child = association.to_a.first # hash should only be of length 1 + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - loaders = preloaders_for_one parent, records, scope - - recs = loaders.flat_map(&:preloaded_records).uniq - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } end @@ -140,36 +141,14 @@ module ActiveRecord end def grouped_records(association, records) - reflection_records = records_by_reflection(association, records) - - reflection_records.each_with_object({}) do |(reflection, r_records),h| - h[reflection] = r_records.group_by { |record| - association_klass(reflection, record) - } - end - end - - def records_by_reflection(association, records) - records.group_by do |record| - reflection = record.class.reflect_on_association(association) - - reflection || raise_config_error(record, association) - end - end - - def raise_config_error(record, association) - raise ActiveRecord::ConfigurationError, - "Association named '#{association}' was not found on #{record.class.name}; " \ - "perhaps you misspelled it?" - end - - def association_klass(reflection, record) - if reflection.macro == :belongs_to && reflection.options[:polymorphic] - klass = record.read_attribute(reflection.foreign_type.to_s) - klass && klass.constantize - else - reflection.klass + h = {} + records.each do |record| + next unless record + assoc = record.association(association) + klasses = h[assoc.reflection] ||= {} + (klasses[assoc.klass] ||= []) << record end + h end class AlreadyLoaded @@ -198,6 +177,7 @@ module ActiveRecord if owners.first.association(reflection.name).loaded? return AlreadyLoaded end + reflection.check_preloadable! case reflection.macro when :has_many diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 69b65982b3..c0639742be 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -57,9 +57,15 @@ module ActiveRecord end def owners_by_key - @owners_by_key ||= owners.group_by do |owner| - owner[owner_key_name] - end + @owners_by_key ||= if key_conversion_required? + owners.group_by do |owner| + owner[owner_key_name].to_s + end + else + owners.group_by do |owner| + owner[owner_key_name] + end + end end def options @@ -93,13 +99,28 @@ module ActiveRecord records_by_owner end + def key_conversion_required? + association_key_type != owner_key_type + end + + def association_key_type + @klass.type_for_attribute(association_key_name.to_s).type + end + + def owner_key_type + @model.type_for_attribute(owner_key_name.to_s).type + end + def load_slices(slices) @preloaded_records = slices.flat_map { |slice| records_for(slice) } @preloaded_records.map { |record| - [record, record[association_key_name]] + key = record[association_key_name] + key = key.to_s if key_conversion_required? + + [record, key] } end @@ -111,12 +132,15 @@ module ActiveRecord scope = klass.unscoped values = reflection_scope.values + reflection_binds = reflection_scope.bind_values preload_values = preload_scope.values + preload_binds = preload_scope.bind_values scope.where_values = Array(values[:where]) + Array(preload_values[:where]) scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + scope.bind_values = (reflection_binds + preload_binds) - scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] if preload_values.key? :order @@ -127,6 +151,10 @@ module ActiveRecord end end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 2a8530af62..1fed7f74e7 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -23,7 +23,7 @@ module ActiveRecord reset_association owners, through_reflection.name - middle_records = through_records.map { |(_,rec)| rec }.flatten + middle_records = through_records.flat_map { |(_,rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, @@ -84,7 +84,7 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope.order! reflection_scope.values[:order] if scope.eager_loading? + scope = scope.order reflection_scope.values[:order] if scope.eager_loading? end scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 399aff378a..f2e3a4e40f 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -18,11 +18,11 @@ module ActiveRecord end def create(attributes = {}, &block) - create_record(attributes, &block) + _create_record(attributes, &block) end def create!(attributes = {}, &block) - create_record(attributes, true, &block) + _create_record(attributes, true, &block) end def build(attributes = {}) @@ -38,8 +38,23 @@ module ActiveRecord scope.scope_for_create.stringify_keys.except(klass.primary_key) end + def get_records + return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge(as.scope(self, conn)).limit(1) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end + def find_target - if record = scope.take + if record = get_records.first set_inverse_instance record end end @@ -52,7 +67,7 @@ module ActiveRecord replace(record) end - def create_record(attributes, raise_error = false) + def _create_record(attributes, raise_error = false) record = build_record(attributes) yield(record) if block_given? saved = record.save diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ba7d2a3782..611d471e62 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, :chain, :to => :reflection + delegate :source_reflection, :through_reflection, :to => :reflection protected @@ -13,10 +13,12 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain.drop(1).each do |reflection| + reflection.chain.drop(1).each do |reflection| + relation = reflection.klass.all + relation.merge!(reflection.scope) if reflection.scope + scope.merge!( - reflection.klass.all. - except(:select, :create_with, :includes, :preload, :joins, :eager_load) + relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end scope @@ -39,12 +41,16 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } - } + if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + join_attributes = { source_reflection.name => records } + else + join_attributes = { + source_reflection.foreign_key => + records.map { |record| + record.send(source_reflection.association_primary_key(reflection.klass)) + } + } + end if options[:source_type] join_attributes[source_reflection.foreign_type] = @@ -61,18 +67,17 @@ module ActiveRecord # Note: this does not capture all cases, for example it would be crazy to try to # properly support stale-checking for nested associations. def stale_state - if through_reflection.macro == :belongs_to + if through_reflection.belongs_to? owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - through_reflection.macro == :belongs_to && - !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable - if source_reflection.macro != :belongs_to + unless source_reflection.belongs_to? raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb new file mode 100644 index 0000000000..6d38224830 --- /dev/null +++ b/activerecord/lib/active_record/attribute.rb @@ -0,0 +1,120 @@ +module ActiveRecord + class Attribute # :nodoc: + class << self + def from_database(name, value, type) + FromDatabase.new(name, value, type) + end + + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) + end + end + + attr_reader :name, :value_before_type_cast, :type + + # This method should not be called directly. + # Use #from_database or #from_user + def initialize(name, value_before_type_cast, type) + @name = name + @value_before_type_cast = value_before_type_cast + @type = type + end + + def value + # `defined?` is cheaper than `||=` when we get back falsy values + @value = type_cast(value_before_type_cast) unless defined?(@value) + @value + end + + def value_for_database + type.type_cast_for_database(value) + end + + def changed_from?(old_value) + type.changed?(old_value, value, value_before_type_cast) + end + + def changed_in_place_from?(old_value) + type.changed_in_place?(old_value, value) + end + + def with_value_from_user(value) + self.class.from_user(name, value, type) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def type_cast + raise NotImplementedError + end + + def initialized? + true + end + + protected + + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end + end + + class FromDatabase < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_database(value) + end + end + + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_user(value) + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def value_for_database + end + + def initialized? + false + end + end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized + end +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 30fa2c8ba5..2887db3bf7 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -11,6 +11,15 @@ module ActiveRecord # If the passed hash responds to <tt>permitted?</tt> method and the return value # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> # exception is raised. + # + # cat = Cat.new(name: "Gorby", status: "yawning") + # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} + # cat.assign_attributes(status: "sleeping") + # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } + # + # New attributes will be persisted in the database when the object is saved. + # + # Aliased to <tt>attributes=</tt>. def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." @@ -106,7 +115,7 @@ module ActiveRecord end class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column + attr_reader :object, :name, :values, :cast_type def initialize(object, name, values) @object = object @@ -117,22 +126,22 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) - klass = column.klass + @cast_type = object.type_for_attribute(name) + klass = cast_type.klass if klass == Time read_time elsif klass == Date read_date else - read_other(klass) + read_other end end private def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) + if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) Time.zone.local(*set_values) else Time.send(object.class.default_timezone, *set_values) @@ -140,9 +149,9 @@ module ActiveRecord end def read_time - # If column is a :time (and not :date or :timestamp) there is no need to validate if + # If column is a :time (and not :date or :datetime) there is no need to validate if # there are year/month/day fields - if column.type == :time + if cast_type.type == :time # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| values[key] ||= value @@ -172,13 +181,12 @@ module ActiveRecord end end - def read_other(klass) + def read_other max_position = extract_max_param positions = (1..max_position) validate_required_parameters!(positions) - set_values = values.values_at(*positions) - klass.new(*set_values) + values.slice(*positions) end # Checks whether some blank date parameter exists. Note that this is different diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb new file mode 100644 index 0000000000..5b96623b6e --- /dev/null +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -0,0 +1,66 @@ +module ActiveRecord + module AttributeDecorators # :nodoc: + extend ActiveSupport::Concern + + included do + class_attribute :attribute_type_decorations, instance_accessor: false # :internal: + self.attribute_type_decorations = TypeDecorator.new + end + + module ClassMethods # :nodoc: + def decorate_attribute_type(column_name, decorator_name, &block) + matcher = ->(name, _) { name == column_name.to_s } + key = "_#{column_name}_#{decorator_name}" + decorate_matching_attribute_types(matcher, key, &block) + end + + def decorate_matching_attribute_types(matcher, decorator_name, &block) + clear_caches_calculated_from_columns + decorator_name = decorator_name.to_s + + # Create new hashes so we don't modify parent classes + self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block]) + end + + private + + def add_user_provided_columns(*) + super.map do |column| + decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) + column.with_type(decorated_type) + end + end + end + + class TypeDecorator # :nodoc: + delegate :clear, to: :@decorations + + def initialize(decorations = {}) + @decorations = decorations + end + + def merge(*args) + TypeDecorator.new(@decorations.merge(*args)) + end + + def apply(name, type) + decorations = decorators_for(name, type) + decorations.inject(type) do |new_type, block| + block.call(new_type) + end + end + + private + + def decorators_for(name, type) + matching(name, type).map(&:last) + end + + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9326c9c117..a2bb78dfcc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,6 +18,8 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization + + delegate :column_for_attribute, to: :class end AttrNames = Module.new { @@ -29,6 +31,8 @@ module ActiveRecord end } + BLACKLISTED_CLASS_METHODS = %w(private public protected) + class AttributeMethodCache def initialize @module = Module.new @@ -46,7 +50,11 @@ module ActiveRecord end private - def method_body; raise NotImplementedError; end + + # Override this method in the subclasses for method body. + def method_body(method_name, const_name) + raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." + end end module ClassMethods @@ -64,7 +72,8 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: - # Use a mutex; we don't want two thread simultaneously trying to define + return false if @attribute_methods_generated + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated @@ -106,7 +115,8 @@ module ActiveRecord 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 + base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) + defined && !base_defined || super end end @@ -131,7 +141,7 @@ module ActiveRecord # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) - class_method_defined_within?(method_name, Base) + BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc @@ -146,16 +156,6 @@ module ActiveRecord end end - def find_generated_attribute_method(method_name) # :nodoc: - klass = self - until klass == Base - gen_methods = klass.generated_attribute_methods - return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) - klass = klass.superclass - end - nil - end - # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -184,23 +184,29 @@ module ActiveRecord [] end end - end - # If we haven't generated any methods yet, generate them, then - # see if we've created the method we're looking for. - def method_missing(method, *args, &block) # :nodoc: - self.class.define_attribute_methods - if respond_to_without_attributes?(method) - # make sure to invoke the correct attribute method, as we might have gotten here via a `super` - # call in a overwritten attribute method - if attribute_method = self.class.find_generated_attribute_method(method) - # this is probably horribly slow, but should only happen at most once for a given AR class - attribute_method.bind(self).call(*args, &block) - else - send(method, *args, &block) + # Returns the column object for the named attribute. + # Returns nil if the named attribute does not exist. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter + # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # + # person.column_for_attribute(:nothing) + # # => nil + def column_for_attribute(name) + column = columns_hash[name.to_s] + if column.nil? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `column_for_attribute` will return a null object for non-existent columns + in Rails 5.0. Use `has_attribute?` if you need to check for an + attribute's existence. + MESSAGE end - else - super + column end end @@ -221,18 +227,14 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + return false unless super name = name.to_s - self.class.define_attribute_methods - result = super - - # If the result is false the answer is false. - return false unless result # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && self.class.column_names.include?(name) return has_attribute?(name) end @@ -249,7 +251,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) + @attributes.key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -273,14 +275,7 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } - end - - # Placeholder so it can be overriden when needed by serialization - def attributes_for_coder # :nodoc: - attributes + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -323,39 +318,24 @@ module ActiveRecord # class Task < ActiveRecord::Base # end # - # person = Task.new(title: '', is_done: false) - # person.attribute_present?(:title) # => false - # person.attribute_present?(:is_done) # => true - # person.name = 'Francesco' - # person.is_done = true - # person.attribute_present?(:title) # => true - # person.attribute_present?(:is_done) # => true + # task = Task.new(title: '', is_done: false) + # task.attribute_present?(:title) # => false + # task.attribute_present?(:is_done) # => true + # task.title = 'Buy milk' + # task.is_done = true + # task.attribute_present?(:title) # => true + # task.attribute_present?(:is_done) # => true 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. Returns +nil+ if the - # named attribute not exists. - # - # class Person < ActiveRecord::Base - # end - # - # person = Person.new - # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> - # - # person.column_for_attribute(:nothing) - # # => nil - def column_for_attribute(name) - # FIXME: should this return a null object for columns that don't exist? - self.class.columns_hash[name.to_s] - 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 date column is cast to a date object, like Date.new(2004, 12, 12)). It raises # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. # + # Note: +:id+ is always present. + # # Alias for the <tt>read_attribute</tt> method. # # class Person < ActiveRecord::Base @@ -389,13 +369,6 @@ module ActiveRecord protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc: - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) - end - attributes - end - def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value @@ -413,7 +386,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@attributes) && @attributes.key?(attr_name) end private @@ -432,16 +405,16 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !readonly_attribute?(name) + attribute_names.reject do |name| + readonly_attribute?(name) end end # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? end end @@ -450,13 +423,10 @@ module ActiveRecord end def pk_attribute?(name) - column_for_attribute(name).primary + name == self.class.primary_key end def typecasted_attribute_value(name) - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already typecasted, this code - # could be simplified read_attribute(name) end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index f596a8b02e..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,7 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @attributes[attr_name.to_s] + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -57,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 8a1b199997..b58295a106 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,28 +34,47 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - reset_changes + clear_changes_information end end - def initialize_dup(other) # :nodoc: - super - init_changed_attributes - end + def initialize_dup(other) # :nodoc: + super + calculate_changes_from_defaults + end + + def changed? + super || changed_in_place.any? + end + + def changed + super | changed_in_place + end - private - def initialize_internals_callback + def attribute_changed?(attr_name, options = {}) + result = super + # We can't change "from" something in place. Only setters can define + # "from" and "to" + result ||= changed_in_place?(attr_name) unless options.key?(:from) + result + end + + def changes_applied + super + store_original_raw_attributes + end + + def clear_changes_information super - init_changed_attributes + original_raw_attributes.clear end - def init_changed_attributes + private + + def calculate_changes_from_defaults @changed_attributes = nil - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # optimistic locking) won't get written unless they get marked as changed - self.class.columns.each do |c| - attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + self.class.column_defaults.each do |attr, orig_value| + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) end end @@ -63,27 +82,43 @@ module ActiveRecord def write_attribute(attr, value) attr = attr.to_s - save_changed_attribute(attr, value) + old_value = old_attribute_value(attr) - super(attr, value) + result = super + store_original_raw_attribute(attr) + save_changed_attribute(attr, old_value) + result end - def save_changed_attribute(attr, value) - # The attribute already has an unsaved change. + def raw_write_attribute(attr, value) + attr = attr.to_s + + result = super + original_raw_attributes[attr] = value + result + end + + def save_changed_attribute(attr, old_value) + if attribute_changed?(attr) + changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + else + changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + end + end + + def old_attribute_value(attr) if attribute_changed?(attr) - old = changed_attributes[attr] - changed_attributes.delete(attr) unless _field_changed?(attr, old, value) + changed_attributes[attr] else - old = clone_attribute_value(:read_attribute, attr) - changed_attributes[attr] = old if _field_changed?(attr, old, value) + clone_attribute_value(:read_attribute, attr) end end - def update_record(*) + def _update_record(*) partial_writes? ? super(keys_for_partial_write) : super end - def create_record(*) + def _create_record(*) partial_writes? ? super(keys_for_partial_write) : super end @@ -93,34 +128,39 @@ module ActiveRecord changed end - def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(old, value)) - value = nil - else - value = column.type_cast(value) - end + def _field_changed?(attr, old_value) + @attributes[attr].changed_from?(old_value) + end + + def changed_in_place + self.class.attribute_names.select do |attr_name| + changed_in_place?(attr_name) end + end - old != value + def changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) end - def changes_from_nil_to_empty_string?(column, old, value) - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - column.null && (old.nil? || old == 0) && value.blank? + def original_raw_attribute(attr_name) + original_raw_attributes.fetch(attr_name) do + read_attribute_before_type_cast(attr_name) + end end - def changes_from_zero_to_string?(old, value) - # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && non_zero?(value) + def original_raw_attributes + @original_raw_attributes ||= {} end - def non_zero?(value) - value !~ /\A0+(\.0+)?\z/ + def store_original_raw_attribute(attr_name) + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + end + + def store_original_raw_attributes + attribute_names.each do |attr| + store_original_raw_attribute(attr) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 931209b07b..9bd333bbac 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -15,8 +15,10 @@ module ActiveRecord # Returns the primary key value. def id - sync_with_transaction_state - read_attribute(self.class.primary_key) + if pk = self.class.primary_key + sync_with_transaction_state + read_attribute(pk) + end end # Sets the primary key value. @@ -37,6 +39,12 @@ module ActiveRecord read_attribute_before_type_cast(self.class.primary_key) end + # Returns the primary key previous value. + def id_was + sync_with_transaction_state + attribute_was(self.class.primary_key) + end + protected def attribute_method?(attr_name) @@ -52,7 +60,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -81,12 +89,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index d01e9aea59..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -22,7 +22,7 @@ module ActiveRecord # the attribute name. Using a constant means that we do not have # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. + # key the @attributes in read_attribute. def method_body(method_name, const_name) <<-EOMETHOD def #{method_name} @@ -35,35 +35,22 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - - included do - class_attribute :attribute_types_cached_by_default, instance_writer: false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end - module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute - # values should be cached. Usually caching only pays off for attributes - # with expensive conversion methods, like time related columns (e.g. - # +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - cached_attributes.merge attribute_names.map { |attr| attr.to_s } + [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| + define_method method_name do |*| + cached_attributes_deprecation_warning(method_name) + true + end end - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set - end + protected - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) + def cached_attributes_deprecation_warning(method_name) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Calling `#{method_name}` is no longer necessary. All attributes are cached. + MESSAGE end - protected - if Module.methods_transplantable? def define_method_attribute(name) method = ReaderMethodCache[name] @@ -89,45 +76,15 @@ module ActiveRecord end end end - - private - - def cacheable_column?(column) - if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - ! serialized_attributes.include? column.name - else - attribute_types_cached_by_default.include?(column.type) - end - 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 date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes_cache[name] || @attributes_cache.fetch(name) { - column = @column_types_override[name] if @column_types_override - column ||= @column_types[name] - - return @attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } unless column - - value = @attributes.fetch(name) { - return block_given? ? yield(name) : nil - } - - if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) - else - column.type_cast value - end - } + name = self.class.primary_key if name == 'id' + @attributes.fetch_value(name, &block) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 67abbbc2a0..264ce2bdfa 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,20 +3,7 @@ module ActiveRecord 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. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - end - module ClassMethods - ## - # :method: serialized_attributes - # - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - # 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 @@ -30,7 +17,8 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. + # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # or a class name that the object type should be equal to. # # ==== Example # @@ -38,141 +26,43 @@ module ActiveRecord # class User < ActiveRecord::Base # serialize :preferences # end - def serialize(attr_name, class_name = Object) - include Behavior - - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name + # + # # Serialize preferences using JSON as coder. + # class User < ActiveRecord::Base + # serialize :preferences, JSON + # end + # + # # Serialize preferences as Hash using YAML coder. + # class User < ActiveRecord::Base + # serialize :preferences, Hash + # end + def serialize(attr_name, class_name_or_coder = Object) + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + class_name_or_coder else - Coders::YAMLColumn.new(class_name) + Coders::YAMLColumn.new(class_name_or_coder) 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 - end - - class Type # :nodoc: - def initialize(column) - @column = column - end - - def type_cast(value) - if value.state == :serialized - value.unserialized_value @column.type_cast value.value - else - value.unserialized_value - end - end - - def type - @column.type - end - - def accessor - ActiveRecord::Store::IndifferentHashAccessor - end - end - - class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value(v = value) - state == :serialized ? unserialize(v) : value - end - - def serialized_value - state == :unserialized ? serialize : value - end - - def unserialize(v) - self.state = :unserialized - self.value = coder.load(v) - end - - def serialize - self.state = :serialized - self.value = coder.dump(value) - end - end - - # This is only added to the model when serialize is called, which - # ensures we do not make things slower when serialization is not used. - module Behavior # :nodoc: - extend ActiveSupport::Concern - - module ClassMethods # :nodoc: - def initialize_attributes(attributes, options = {}) - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) - - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) - end - end - - attributes - end - end - - def should_record_timestamps? - super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) - end - - def keys_for_partial_write - super | (attributes.keys & self.class.serialized_attributes.keys) - 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 - - def _field_changed?(attr, old, value) - if self.class.serialized_attributes.include?(attr) - old != value - else - super - end - end - - def read_attribute_before_type_cast(attr_name) - if self.class.serialized_attributes.include?(attr_name) - super.unserialized_value - else - super + decorate_attribute_type(attr_name, :serialize) do |type| + Type::Serialized.new(type, coder) end end - def attributes_before_type_cast - super.dup.tap do |attributes| - self.class.serialized_attributes.each_key do |key| - if attributes.key?(key) - attributes[key] = attributes[key].unserialized_value - end - end - end - end - - def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - super - end - end - - def attributes_for_coder - attribute_names.each_with_object({}) do |name, attrs| - attrs[name] = if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - read_attribute(name) - end - end + def serialized_attributes + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + `serialized_attributes` is deprecated without replacement, and will + be removed in Rails 5.0. + WARNING + @serialized_attributes ||= Hash[ + columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| + [c.name, c.cast_type.coder] + } + ] 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 f168282ea3..f439bd1ffe 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,18 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type # :nodoc: - def initialize(column) - @column = column + class TimeZoneConverter < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + convert_time_to_time_zone(super) end - def type_cast(value) - value = @column.type_cast(value) - value.acts_like?(:time) ? value.in_time_zone : value + def type_cast_from_user(value) + if value.is_a?(Array) + value.map { |v| type_cast_from_user(v) } + elsif value.respond_to?(:in_time_zone) + value.in_time_zone + end end - def type - @column.type + def convert_time_to_time_zone(value) + if value.is_a?(Array) + value.map { |v| convert_time_to_time_zone(v) } + elsif value.acts_like?(:time) + value.in_time_zone + else + value + end end end @@ -27,31 +36,26 @@ module ActiveRecord end module ClassMethods - protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the 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}=(time) - time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil - previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) - write_attribute(:#{attr_name}, time) - #{attr_name}_will_change! if previous_time != time_with_zone - @attributes_cache["#{attr_name}"] = time_with_zone - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super + private + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end end + super end - private - def create_time_zone_conversion_attribute?(name, column) + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type || :timestamp == column.type) + (:datetime == cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c853fc0917..b3c8209a74 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -26,8 +26,6 @@ module ActiveRecord protected if Module.methods_transplantable? - # See define_method_attribute in read.rb for an explanation of - # this code. def define_method_attribute=(name) method = WriterMethodCache[name] generated_attribute_methods.module_eval { @@ -55,24 +53,12 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) - column = column_for_attribute(attr_name) - - # If we're dealing with a binary column, write the data to the cache - # so we don't attempt to typecast multiple times. - if column && column.binary? - @attributes_cache[attr_name] = value - end + write_attribute_with_type_cast(attr_name, value, true) + end - 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 + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, false) end - alias_method :raw_write_attribute, :write_attribute private # Handle *= for method_missing. @@ -80,10 +66,17 @@ module ActiveRecord write_attribute(attribute_name, value) end - def type_cast_attribute_for_write(column, value) - return value unless column + def write_attribute_with_type_cast(attr_name, value, should_type_cast) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key + + if should_type_cast + @attributes.write_from_user(attr_name, value) + else + @attributes.write_from_database(attr_name, value) + end - column.type_cast_for_write value + value end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb new file mode 100644 index 0000000000..98ac63c7e1 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set.rb @@ -0,0 +1,77 @@ +require 'active_record/attribute_set/builder' + +module ActiveRecord + class AttributeSet # :nodoc: + delegate :keys, to: :initialized_attributes + + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def fetch_value(name, &block) + self[name].value(&block) + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def freeze + @attributes.freeze + super + end + + def initialize_dup(_) + @attributes = attributes.transform_values(&:dup) + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if key?(key) + write_from_database(key, nil) + end + end + + def ensure_initialized(key) + unless self[key].initialized? + write_from_database(key, nil) + end + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..1e146a07da --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types + + def initialize(types) + @types = types + end + + def build_from_database(values = {}, additional_types = {}) + attributes = build_attributes_from_values(values, additional_types) + add_uninitialized_attributes(attributes) + AttributeSet.new(attributes) + end + + private + + def build_attributes_from_values(values, additional_types) + values.each_with_object({}) do |(name, value), hash| + type = additional_types.fetch(name, types[name]) + hash[name] = Attribute.from_database(name, value, type) + end + end + + def add_uninitialized_attributes(attributes) + types.except(*attributes.keys).each do |name, type| + attributes[name] = Attribute.uninitialized(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb new file mode 100644 index 0000000000..890a1314d9 --- /dev/null +++ b/activerecord/lib/active_record/attributes.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module Attributes # :nodoc: + extend ActiveSupport::Concern + + Type = ActiveRecord::Type + + included do + class_attribute :user_provided_columns, instance_accessor: false # :internal: + self.user_provided_columns = {} + end + + module ClassMethods # :nodoc: + # Defines or overrides a attribute on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # +name+ The name of the methods to define attribute methods for, and the column which + # this will persist to. + # + # +cast_type+ A type object that contains information about how to type cast the value. + # See the examples section for more information. + # + # ==== Options + # The options hash accepts the following options: + # + # +default+ is the default value that the column should use on a new record. + # + # ==== Examples + # + # The type detected by Active Record can be overridden. + # + # # db/schema.rb + # create_table :store_listings, force: true do |t| + # t.decimal :price_in_cents + # end + # + # # app/models/store_listing.rb + # class StoreListing < ActiveRecord::Base + # end + # + # store_listing = StoreListing.new(price_in_cents: '10.1') + # + # # before + # store_listing.price_in_cents # => BigDecimal.new(10.1) + # + # class StoreListing < ActiveRecord::Base + # attribute :price_in_cents, Type::Integer.new + # end + # + # # after + # store_listing.price_in_cents # => 10 + # + # Users may also define their own custom types, as long as they respond to the methods + # defined on the value type. The `type_cast` method on your type object will be called + # with values both from the database, and from your controllers. See + # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your + # type objects inherit from an existing type, or the base value type. + # + # class MoneyType < ActiveRecord::Type::Integer + # def type_cast(value) + # if value.include?('$') + # price_in_dollars = value.gsub(/\$/, '').to_f + # price_in_dollars * 100 + # else + # value.to_i + # end + # end + # end + # + # class StoreListing < ActiveRecord::Base + # attribute :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def attribute(name, cast_type, options = {}) + name = name.to_s + clear_caches_calculated_from_columns + # Assign a new hash to ensure that subclasses do not share a hash + self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type)) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + 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 + + def reset_column_information # :nodoc: + super + clear_caches_calculated_from_columns + end + + private + + def add_user_provided_columns(schema_columns) + existing_columns = schema_columns.map do |column| + user_provided_columns[column.name] || column + end + + existing_column_names = existing_columns.map(&:name) + new_columns = user_provided_columns.except(*existing_column_names).values + + existing_columns + new_columns + end + + def clear_caches_calculated_from_columns + @attributes_builder = nil + @column_names = nil + @column_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = nil + end + end + end +end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4f58d06f35..dd92e29199 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -35,7 +35,7 @@ module ActiveRecord # # === One-to-one Example # - # class Post + # class Post < ActiveRecord::Base # has_one :author, autosave: true # end # @@ -76,7 +76,7 @@ module ActiveRecord # # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved: # - # class Post + # class Post < ActiveRecord::Base # has_many :comments # :autosave option is not declared # end # @@ -95,20 +95,23 @@ module ActiveRecord # When <tt>:autosave</tt> is true all children are saved, no matter whether they # are new records or not: # - # class Post + # class Post < ActiveRecord::Base # has_many :comments, autosave: true # end # # post = Post.create(title: 'ruby rocks') # post.comments.create(body: 'hello world') # post.comments[0].body = 'hi everyone' - # post.save # => saves both post and comment, with 'hi everyone' as body + # post.comments.build(body: "good morning.") + # post.title += "!" + # post.save # => saves both post and comments. # # Destroying one of the associated models as part of the parent's save action # is as simple as marking it for destruction: # - # post.comments.last.mark_for_destruction - # post.comments.last.marked_for_destruction? # => true + # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]> + # post.comments[1].mark_for_destruction + # post.comments[1].marked_for_destruction? # => true # post.comments.length # => 2 # # Note that the model is _not_ yet removed from the database: @@ -144,6 +147,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) + return if method_defined?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -176,30 +180,26 @@ module ActiveRecord validation_method = :"validate_associated_records_for_#{reflection.name}" collection = reflection.collection? - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method) { save_collection_association(reflection) } - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create save_method - after_update save_method - elsif reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } - before_save save_method - end + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + after_save save_method + elsif reflection.has_one? + define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method + else + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method end if reflection.validate? && !method_defined?(validation_method) @@ -270,9 +270,11 @@ module ActiveRecord # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end end @@ -301,7 +303,8 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid?(self.validation_context) + validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" @@ -359,6 +362,7 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end + @new_record_before_save = false end # reconstruct the scope now that we know the owner's id @@ -377,15 +381,16 @@ module ActiveRecord def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy - else + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9ec1feea97..f978fbd0a4 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,16 +9,19 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/class/subclasses' require 'arel' +require 'active_record/attribute_decorators' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/attributes' module ActiveRecord #:nodoc: # = Active Record @@ -138,6 +141,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. + # For numeric values, present is defined as non-zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -217,25 +221,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # @@ -296,7 +284,6 @@ module ActiveRecord #:nodoc: include Core include Persistence - include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance @@ -307,6 +294,8 @@ module ActiveRecord #:nodoc: include Integration include Validations include CounterCache + include Attributes + include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic include AttributeMethods @@ -318,6 +307,7 @@ module ActiveRecord #:nodoc: include NestedAttributes include Aggregations include Transactions + include NoTouching include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 35f19f0bc0..5955673b42 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -302,11 +302,11 @@ module ActiveRecord run_callbacks(:save) { super } end - def create_record #:nodoc: + def _create_record #:nodoc: run_callbacks(:create) { super } end - def update_record(*) #:nodoc: + def _update_record(*) #:nodoc: run_callbacks(:update) { super } end end diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb new file mode 100644 index 0000000000..75d3bfe625 --- /dev/null +++ b/activerecord/lib/active_record/coders/json.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Coders # :nodoc: + class JSON # :nodoc: + def self.dump(obj) + ActiveSupport::JSON.encode(obj) + end + + def self.load(json) + ActiveSupport::JSON.decode(json) unless json.nil? + end + end + end +end 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 759e162e19..a5fa9d6adc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -58,13 +58,11 @@ module ActiveRecord # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). # * +reaping_frequency+: frequency in seconds to periodically run the - # Reaper, which attempts to find and close dead connections, which can - # occur if a programmer forgets to close a connection at the end of a - # thread or a thread dies unexpectedly. (Default nil, which means don't - # run the Reaper). - # * +dead_connection_timeout+: number of seconds from last checkout - # after which the Reaper will consider a connection reapable. (default - # 5 seconds). + # Reaper, which attempts to find and recover connections from dead + # threads, which can occur if a programmer forgets to close a + # connection at the end of a thread or a thread dies unexpectedly. + # Regardless of this setting, the Reaper will be invoked before every + # blocking wait. (Default nil, which means don't schedule the Reaper). class ConnectionPool # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool # with which it shares a Monitor. But could be a generic Queue. @@ -222,7 +220,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout + attr_accessor :automatic_reconnect, :checkout_timeout attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -237,7 +235,6 @@ module ActiveRecord @spec = spec @checkout_timeout = spec.config[:checkout_timeout] || 5 - @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5 @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -361,11 +358,13 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) synchronize do + owner = conn.owner + conn.run_callbacks :checkin do conn.expire end - release conn + release owner @available.add conn end @@ -378,22 +377,28 @@ module ActiveRecord @connections.delete conn @available.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. - release conn + release conn.owner @available.add checkout_new_connection if @available.any_waiting? 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 + # Recover lost connections for the pool. A lost connection can occur if + # a programmer forgets to checkin a connection at the end of a thread # or a thread dies unexpectedly. def reap - synchronize do - stale = Time.now - @dead_connection_timeout - connections.dup.each do |conn| - if conn.in_use? && stale > conn.last_use && !conn.active_threadsafe? + stale_connections = synchronize do + @connections.select do |conn| + conn.in_use? && !conn.owner.alive? + end + end + + stale_connections.each do |conn| + synchronize do + if conn.active? + conn.reset! + checkin conn + else remove conn end end @@ -415,20 +420,15 @@ module ActiveRecord elsif @connections.size < @size checkout_new_connection else + reap @available.poll(@checkout_timeout) end end - def release(conn) - thread_id = if @reserved_connections[current_connection_id] == conn - current_connection_id - else - @reserved_connections.keys.find { |k| - @reserved_connections[k] == conn - } - end + def release(owner) + thread_id = owner.object_id - @reserved_connections.delete thread_id if thread_id + @reserved_connections.delete thread_id end def new_connection @@ -462,23 +462,44 @@ module ActiveRecord # # For example, suppose that you have 5 models, with the following hierarchy: # - # | - # +-- Book - # | | - # | +-- ScaryBook - # | +-- GoodBook - # +-- Author - # +-- BankAccount + # class Author < ActiveRecord::Base + # end + # + # class BankAccount < ActiveRecord::Base + # end + # + # class Book < ActiveRecord::Base + # establish_connection "library_db" + # end + # + # class ScaryBook < Book + # end + # + # class GoodBook < Book + # end + # + # And a database.yml that looked like this: + # + # development: + # database: my_application + # host: localhost + # + # library_db: + # database: library + # host: some.library.org + # + # Your primary database in the development environment is "my_application" + # but the Book model connects to a separate database called "library_db" + # (this can even be a database on a different machine). # - # Suppose that Book is to connect to a separate database (i.e. one other - # than the default database). Then Book, ScaryBook and GoodBook will all use - # the same connection pool. Likewise, Author and BankAccount will use the - # same connection pool. However, the connection pool used by Author/BankAccount - # is not the same as the one used by Book/ScaryBook/GoodBook. + # Book, ScaryBook and GoodBook will all use the same connection pool to + # "library_db" while Author, BankAccount, and any other models you create + # will use the default connection pool to "my_application". # - # Normally there is only a single ConnectionHandler instance, accessible via - # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine the connection pool that they should use. + # The various connection pools are managed by a single instance of + # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. + # All Active Record models use this handler to determine the connection pool that they + # should use. class ConnectionHandler def initialize # These caches are keyed by klass.name, NOT klass. Keying them by klass @@ -538,7 +559,10 @@ module ActiveRecord # for (not necessarily the current class). def retrieve_connection(klass) #:nodoc: pool = retrieve_connection_pool(klass) - (pool && pool.connection) or raise ConnectionNotEstablished + raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool + conn = pool.connection + raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn + conn end # Returns true if a connection that's accessible to this class has 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 6eb59cc398..98e96099cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -9,25 +9,26 @@ module ActiveRecord # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) - binds = binds.dup - visitor.accept(arel.ast) do - quote(*binds.shift.reverse) - end + collected = visitor.accept(arel.ast, collector) + collected.compile(binds.dup, self) else arel end end - # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) - if arel.is_a?(Relation) - relation = arel - arel = relation.arel - if !binds || binds.empty? - binds = relation.bind_values - end + # This is used in the StatementCache object. It returns an object that + # can be used to query the database repeatedly. + def cacheable_query(arel) # :nodoc: + if prepared_statements + ActiveRecord::StatementCache.query visitor, arel.ast + else + ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector end + end + # Returns an ActiveRecord::Result instance. + def select_all(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select(to_sql(arel, binds), name, binds) end @@ -47,10 +48,7 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil) - binds = [] - if arel.is_a?(Relation) - arel, binds = arel.arel, arel.bind_values - end + arel, binds = binds_from_relation arel, [] select_rows(to_sql(arel, binds), name, binds).map(&:first) end @@ -195,7 +193,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) @@ -205,58 +203,30 @@ module ActiveRecord if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end + attr_reader :transaction_manager #:nodoc: - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). @@ -328,7 +298,7 @@ module ActiveRecord def sanitize_limit(limit) if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) limit - elsif limit.to_s =~ /,/ + elsif limit.to_s.include?(',') Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') else Integer(limit) @@ -336,8 +306,8 @@ module ActiveRecord end # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in - # an UPDATE statement, so in the mysql adapters we redefine this to do that. + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: key = update.key subselect = subquery_for(key, select) @@ -389,6 +359,13 @@ module ActiveRecord row = result.rows.first row && row.first end + + def binds_from_relation(relation, binds) + if relation.is_a?(Relation) && binds.empty? + relation, binds = relation.arel, relation.bind_values + end + [relation, binds] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index adc23a6674..4a4506c7f5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -63,6 +63,7 @@ module ActiveRecord def select_all(arel, name = nil, binds = []) if @query_cache_enabled && !locked?(arel) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) cache_sql(sql, binds) { super(sql, name, binds) } else diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..eb88845913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,34 +9,11 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return "'#{quote_string(value)}'" unless column - - case column.type - when :integer then value.to_i.to_s - when :float then value.to_f.to_s - else - "'#{quote_string(value)}'" - end - - when true, false - if column && column.type == :integer - value ? '1' : '0' - else - value ? quoted_true : quoted_false - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s - when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" + if column + value = column.cast_type.type_cast_for_database(value) end + + _quote(value) end # Cast a +value+ to a type that the database understands. For example, @@ -47,34 +24,14 @@ module ActiveRecord return value.id end - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return value unless column - - case column.type - when :integer then value.to_i - when :float then value.to_f - else - value - end - - when true, false - if column && column.type == :integer - value ? 1 : 0 - else - value ? 't' : 'f' - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then nil - when BigDecimal then value.to_s('F') - when Numeric then value - when Date, Time then quoted_date(value) - when Symbol then value.to_s - else - to_type = column ? " to #{column.type}" : "" - raise TypeError, "can't cast #{value.class}#{to_type}" + if column + value = column.cast_type.type_cast_for_database(value) end + + _type_cast(value) + rescue TypeError + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end # Quotes a string, escaping any ' (single quote) and \ (backslash) @@ -99,7 +56,7 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") @@ -109,10 +66,18 @@ module ActiveRecord "'t'" end + def unquoted_true + 't' + end + def quoted_false "'f'" end + def unquoted_false + 'f' + end + def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -124,6 +89,45 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + end + + def _quote(value) + case value + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false + when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Numeric, ActiveSupport::Duration then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" + else + "'#{quote_string(YAML.dump(value))}'" + end + end + + def _type_cast(value) + case value + when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Date, Time then quoted_date(value) + when *types_which_need_no_typecasting + value + else raise TypeError + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index a51691bfa8..adad6cd542 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -13,7 +13,7 @@ module ActiveRecord end def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) sql = "ADD #{quote_column_name(o.name)} #{sql_type}" add_column_options!(sql, column_options(o)) end @@ -23,10 +23,12 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" add_column_options!(column_sql, column_options(o)) unless o.primary_key? column_sql @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -64,7 +81,7 @@ module ActiveRecord end def add_column_options!(sql, options) - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations if options[:null] == false sql << " NOT NULL" @@ -75,9 +92,33 @@ module ActiveRecord sql end + def quote_value(value, column) + column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + column.cast_type ||= type_for_column(column) + + @conn.quote(value, column) + end + def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end + + def type_for_column(column) + @conn.lookup_cast_type(column.sql_type) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index c39bf15e83..e44ccb7d81 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -25,6 +25,37 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -99,9 +130,11 @@ module ActiveRecord # Specifies the precision for a <tt>:decimal</tt> column. # * <tt>:scale</tt> - # Specifies the scale for a <tt>:decimal</tt> column. + # * <tt>:index</tt> - + # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # For clarity's sake: the precision is the number of significant digits, - # while the scale is the number of digits that can be stored following + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can # range from -999.99 to 999.99. @@ -123,17 +156,8 @@ module ActiveRecord # Default is (38,0). # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. # Default unknown. - # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18]. - # Default (9,0). Internal types NUMERIC and DECIMAL have different - # storage rules, decimal being better. - # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for - # NUMERIC is 19, and DECIMAL is 38. # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. # Default (38,0). - # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>. # # This method returns <tt>self</tt>. # @@ -172,18 +196,21 @@ module ActiveRecord # What can be written like this with the regular calls to column: # # create_table :products do |t| - # t.column :shop_id, :integer - # t.column :creator_id, :integer - # t.column :name, :string, default: "Untitled" - # t.column :value, :string, default: "Untitled" - # t.column :created_at, :datetime - # t.column :updated_at, :datetime + # t.column :shop_id, :integer + # t.column :creator_id, :integer + # t.column :item_number, :string + # t.column :name, :string, default: "Untitled" + # t.column :value, :string, default: "Untitled" + # t.column :created_at, :datetime + # t.column :updated_at, :datetime # end + # add_index :products, :item_number # # can also be written as follows using the short-hand: # # create_table :products do |t| # t.integer :shop_id, :creator_id + # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" # t.timestamps # end @@ -219,6 +246,8 @@ module ActiveRecord raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end + index_options = options.delete(:index) + index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options @columns_hash[name] = new_column_definition(name, type, options) self end @@ -251,12 +280,22 @@ module ActiveRecord column(:updated_at, :datetime, options) end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. + # + # t.references(:user) + # t.references(:user, type: "string") + # t.belongs_to(:supplier, polymorphic: true) + # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) + type = options.delete(:type) || :integer args.each do |col| - column("#{col}_id", :integer, options) + column("#{col}_id", type, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -264,6 +303,7 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: + type = aliased_types[type] || type column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) @@ -294,18 +334,36 @@ module ActiveRecord def native @native end + + def aliased_types + HashWithIndifferentAccess.new( + timestamp: :datetime, + ) + end end class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym @@ -452,11 +510,14 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. # # t.references(:user) + # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! args.each do |ref_name| @@ -471,6 +532,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#remove_reference def remove_references(*args) options = args.extract_options! args.each do |ref_name| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index cdf0cbe218..9bd0401e40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,5 +1,3 @@ -require 'ipaddr' - module ActiveRecord module ConnectionAdapters # :nodoc: # The goal of this module is to move Adapter specific column @@ -20,19 +18,13 @@ module ActiveRecord def prepare_column_options(column, types) spec = {} spec[:name] = column.name.inspect - - # 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)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:type] = column.type.to_s + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null - spec[:default] = default_string(column.default) if column.has_default? + spec[:default] = schema_default(column) if column.has_default? + spec.delete(:default) if spec[:default].nil? spec end @@ -43,28 +35,12 @@ module ActiveRecord private - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'#{value.to_s(:db)}'" - when Range - # infinity dumps as Infinity, which causes uninitialized constant error - value.inspect.gsub('Infinity', '::Float::INFINITY') - when IPAddr - subnet_mask = value.instance_variable_get(:@mask_addr) - - # If the subnet mask is equal to /32, don't output it - if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" - else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" - end - else - value.inspect - end + def schema_default(column) + default = column.type_cast_from_database(column.default) + unless default.nil? + column.type_cast_for_schema(default) end + end end end end 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 ad069f5e53..4957e1ac80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -40,16 +40,17 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, unique: true) # # # Check an index with a custom name exists - # index_exists?(:suppliers, :company_id, name: "idx_company_id" + # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - 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 } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + column_names = Array(column_name).map(&:to_s) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) + checks = [] + checks << lambda { |i| i.name == index_name } + checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| i.unique } if options[:unique] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. @@ -71,7 +72,8 @@ module ActiveRecord # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # def column_exists?(table_name, column_name, type = nil, options = {}) - columns(table_name).any?{ |c| c.name == column_name.to_s && + column_name = column_name.to_s + columns(table_name).any?{ |c| c.name == column_name && (!type || c.type == type) && (!options.key?(:limit) || c.limit == options[:limit]) && (!options.key?(:precision) || c.precision == options[:precision]) && @@ -186,24 +188,23 @@ module ActiveRecord def create_table(table_name, options = {}) td = create_table_definition table_name, options[:temporary], options[:options], options[:as] - if !options[:as] - unless options[:id] == false - pk = options.fetch(:primary_key) { - Base.get_primary_key table_name.to_s.singularize - } - - td.primary_key pk, options.fetch(:id, :primary_key), options + if options[:id] != false && !options[:as] + pk = options.fetch(:primary_key) do + Base.get_primary_key table_name.to_s.singularize end - yield td if block_given? + td.primary_key pk, options.fetch(:id, :primary_key), options end + yield td if block_given? + if options[:force] && table_exists?(table_name) drop_table(table_name, options) end - execute schema_creation.accept td - td.indexes.each_pair { |c,o| add_index table_name, c, o } + result = execute schema_creation.accept td + td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + result end # Creates a new join table with the name created using the lexical order of the first two @@ -602,12 +603,18 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify + # a different type. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # - # ====== Create a user_id column + # ====== Create a user_id integer column # # add_reference(:products, :user) # + # ====== Create a user_id string column + # + # add_reference(:products, :user, type: :string) + # # ====== Create a supplier_id and supplier_type columns # # add_belongs_to(:products, :supplier, polymorphic: true) @@ -619,7 +626,8 @@ module ActiveRecord def add_reference(table_name, ref_name, options = {}) polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) - add_column(table_name, "#{ref_name}_id", :integer, options) + type = options.delete(:type) || :integer + add_column(table_name, "#{ref_name}_id", type, options) add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -642,6 +650,115 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -740,6 +857,40 @@ module ActiveRecord Table.new(table_name, base) end + def add_index_options(table_name, column_name, options = {}) #:nodoc: + column_names = Array(column_name) + index_name = index_name(table_name, column: column_names) + + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) + + index_type = options[:unique] ? "UNIQUE" : "" + index_type = options[:type].to_s if options.key?(:type) + index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + + if options.key?(:algorithm) + algorithm = index_algorithms.fetch(options[:algorithm]) { + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + } + end + + using = "USING #{options[:using]}" if options[:using].present? + + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end + + if index_name.length > max_index_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" + end + if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns, index_options, algorithm, using] + end + protected def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -754,7 +905,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] @@ -770,40 +921,6 @@ module ActiveRecord options.include?(:default) && !(options[:null] == false && options[:default].nil?) end - def add_index_options(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = index_name(table_name, column: column_names) - - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - - index_type = options[:unique] ? "UNIQUE" : "" - index_type = options[:type].to_s if options.key?(:type) - index_name = options[:name].to_s if options.key?(:name) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length - - if options.key?(:algorithm) - algorithm = index_algorithms.fetch(options[:algorithm]) { - raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") - } - end - - using = "USING #{options[:using]}" if options[:using].present? - - if supports_partial_index? - index_options = options[:where] ? " WHERE #{options[:where]}" : "" - end - - if index_name.length > max_index_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" - end - if index_name_exists?(table_name, index_name, false) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" - end - index_columns = quoted_columns_for_index(column_names, options).join(", ") - - [index_name, index_type, index_columns, index_options, algorithm, using] - end - def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) @@ -852,6 +969,12 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..4a7f2aaca8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,20 +1,7 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection - - def initialize(connection) - @connection = connection - @state = TransactionState.new - end - - def state - @state - end - end - class TransactionState - attr_accessor :parent + attr_reader :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) @@ -43,82 +30,24 @@ module ActiveRecord end end - class ClosedTransaction < Transaction #:nodoc: - def number - 0 - end - - def begin(options = {}) - RealTransaction.new(connection, self, options) - end - - def closed? - true - end - - def open? - false - end - - def joinable? - false - end - - # This is a noop when there are no open transactions - def add_record(record) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable - - def initialize(connection, parent, options = {}) - super connection - - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) - end - - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing - end - - def joinable? - @joinable && !finishing? - end - - def number - if finishing? - parent.number - else - parent.number + 1 - end - end - - def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) - end - end + class Transaction #:nodoc: - def rollback - @finishing = true - perform_rollback - parent - end + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - def commit - @finishing = true - perform_commit - parent + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end def add_record(record) @@ -129,19 +58,25 @@ module ActiveRecord end end - def rollback_records + def rollback @state.set_state(:rolledback) + end + + def rollback_records records.uniq.each do |record| begin - record.rolledback!(parent.closed?) + record.rolledback! full_rollback? rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end end - def commit_records + def commit @state.set_state(:committed) + end + + def commit_records records.uniq.each do |record| begin record.committed! @@ -151,19 +86,40 @@ module ActiveRecord end end - def closed? - false + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + connection.create_savepoint(@savepoint_name = savepoint_name) + end + + def rollback + super + connection.rollback_to_savepoint(savepoint_name) + rollback_records end - def open? - true + def commit + super + connection.release_savepoint(savepoint_name) end + + def full_rollback?; false; end end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - super + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -171,37 +127,71 @@ module ActiveRecord end end - def perform_rollback + def rollback + super connection.rollback_db_transaction rollback_records end - def perform_commit + def commit + super connection.commit_db_transaction commit_records end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" - end + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end - super - connection.create_savepoint + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + end + @stack.push(transaction) + transaction end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def commit_transaction + @stack.pop.commit end - def perform_commit - @state.set_state(:committed) - @state.parent = parent.state - connection.release_savepoint + def rollback_transaction + @stack.pop.rollback + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + transaction.rollback if transaction + raise + ensure + begin + transaction.commit unless error + rescue Exception + transaction.rollback + raise + ensure + @stack.pop if transaction + end end + + def open_transactions + @stack.size + end + + def current_transaction + @stack.last || NULL_TRANSACTION + end + + private + NULL_TRANSACTION = NullTransaction.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 11b28a4858..a1b6671664 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,17 +1,23 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' +require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' +require 'arel/collectors/bind' +require 'arel/collectors/sql_string' module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload :Column + autoload_at 'active_record/connection_adapters/column' do + autoload :Column + autoload :NullColumn + end autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do @@ -39,7 +45,8 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do - autoload :ClosedTransaction + autoload :TransactionManager + autoload :NullTransaction autoload :RealTransaction autoload :SavepointTransaction autoload :TransactionState @@ -71,8 +78,8 @@ module ActiveRecord define_callbacks :checkout, :checkin attr_accessor :visitor, :pool - attr_reader :schema_cache, :last_use, :in_use, :logger - alias :in_use? :in_use + attr_reader :schema_cache, :owner, :logger + alias :in_use? :owner def self.type_cast_config_to_integer(config) if config =~ SIMPLE_INT @@ -90,13 +97,14 @@ module ActiveRecord end end + attr_reader :prepared_statements + def initialize(connection, logger = nil, pool = nil) #:nodoc: super() @connection = connection - @in_use = false + @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter - @last_use = false @logger = logger @pool = pool @schema_cache = SchemaCache.new self @@ -104,6 +112,26 @@ module ActiveRecord @prepared_statements = false end + class BindCollector < Arel::Collectors::Bind + def compile(bvs, conn) + super(bvs.map { |bv| conn.quote(*bv.reverse) }) + end + end + + class SQLString < Arel::Collectors::SQLString + def compile(bvs, conn) + super(bvs) + end + end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + def valid_type?(type) true end @@ -114,9 +142,8 @@ module ActiveRecord def lease synchronize do - unless in_use - @in_use = true - @last_use = Time.now + unless in_use? + @owner = Thread.current end end end @@ -127,19 +154,14 @@ module ActiveRecord end def expire - @in_use = false - end - - def unprepared_visitor - self.class::BindSubstitution.new self + @owner = nil end def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false - old_visitor, @visitor = @visitor, unprepared_visitor yield ensure - @visitor, @prepared_statements = old_visitor, old_prepared_statements + @prepared_statements = old_prepared_statements end # Returns the human-readable name of the adapter. Use mixed case - one @@ -148,28 +170,19 @@ module ActiveRecord 'Abstract' end - # Does this adapter support migrations? Backend specific, as the - # abstract adapter always returns +false+. + # Does this adapter support migrations? def supports_migrations? false end # Can this adapter determine the primary key for tables not attached - # to an Active Record class, such as join tables? Backend specific, as - # the abstract adapter always returns +false+. + # to an Active Record class, such as join tables? def supports_primary_key? false end - # Does this adapter support using DISTINCT within COUNT? This is +true+ - # for all adapters except sqlite. - def supports_count_distinct? - true - end - # Does this adapter support DDL rollbacks in transactions? That is, would - # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL, - # SQL Server, and others support this. MySQL and others do not. + # CREATE TABLE or ALTER TABLE get rolled back by a transaction? def supports_ddl_transactions? false end @@ -178,8 +191,7 @@ module ActiveRecord false end - # Does this adapter support savepoints? PostgreSQL and MySQL do, - # SQLite < 3.6.8 does not. + # Does this adapter support savepoints? def supports_savepoints? false end @@ -187,7 +199,6 @@ module ActiveRecord # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. - # This is false for all adapters but Firebird. def prefetch_primary_key?(table_name = nil) false end @@ -202,8 +213,7 @@ module ActiveRecord false end - # Does this adapter support explain? As of this writing sqlite3, - # mysql2, and postgresql are the only ones that do. + # Does this adapter support explain? def supports_explain? false end @@ -213,12 +223,22 @@ module ActiveRecord false end - # Does this adapter support database extensions? As of this writing only - # postgresql does. + # Does this adapter support database extensions? def supports_extensions? false end + # Does this adapter support creating indexes in the same statement as + # creating the table? + def supports_indexes_in_create? + false + end + + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -227,14 +247,12 @@ module ActiveRecord def enable_extension(name) end - # A list of extensions, to be filled in by adapters that support them. At - # the moment only postgresql does. + # A list of extensions, to be filled in by adapters that support them. def extensions [] end # A list of index algorithms, to be filled by adapters that support them. - # MySQL and PostgreSQL have support for them right now. def index_algorithms {} end @@ -262,12 +280,6 @@ module ActiveRecord def active? end - # Adapter should redefine this if it needs a threadsafe way to approximate - # if the connection is active - def active_threadsafe? - active? - end - # Disconnects from the database if already connected, and establishes a # new connection with the database. Implementors should call super if they # override the default implementation. @@ -301,7 +313,6 @@ module ActiveRecord end # Returns true if its required to reload the connection between requests for development mode. - # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite. def requires_reloading? false end @@ -323,10 +334,6 @@ module ActiveRecord @connection end - def open_transactions - @transaction.number - end - def create_savepoint(name = nil) end @@ -336,16 +343,22 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) node end + def case_sensitive_comparison(table, attribute, column, value) + table_attr = table[attribute] + value = case_sensitive_modifier(value, table_attr) unless value.nil? + table_attr.eq(value) + end + def case_insensitive_comparison(table, attribute, column, value) table[attribute].lower.eq(table.lower(value)) end def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool @@ -353,8 +366,80 @@ module ActiveRecord pool.checkin self end + def type_map # :nodoc: + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end + + def new_column(name, default, cast_type, sql_type = nil, null = true) + Column.new(name, default, cast_type, sql_type, null) + end + + def lookup_cast_type(sql_type) # :nodoc: + type_map.lookup(sql_type) + end + protected + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_limit m, %r(date)i, Type::Date + register_class_with_limit m, %r(time)i, Type::Time + register_class_with_limit m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer + + m.alias_type %r(blob)i, 'binary' + m.alias_type %r(clob)i, 'text' + m.alias_type %r(timestamp)i, 'datetime' + m.alias_type %r(numeric)i, 'decimal' + m.alias_type %r(number)i, 'decimal' + m.alias_type %r(double)i, 'float' + + m.register_type(%r(decimal)i) do |sql_type| + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + + if scale == 0 + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) + else + Type::Decimal.new(precision: precision, scale: scale) + end + end + end + + def reload_type_map # :nodoc: + type_map.clear + initialize_type_map(type_map) + end + + def register_class_with_limit(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + limit = extract_limit(args.last) + klass.new(limit: limit) + end + end + + def extract_scale(sql_type) # :nodoc: + case sql_type + when /\((\d+)\)/ then 0 + when /\((\d+)(,(\d+))\)/ then $3.to_i + end + end + + def extract_precision(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ + end + + def extract_limit(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((.*)\)/ + end + def translate_exception_class(e, sql) message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger @@ -381,7 +466,13 @@ module ActiveRecord end def without_prepared_statement?(binds) - !@prepared_statements || binds.empty? + !prepared_statements || binds.empty? + end + + def column_for(table_name, column_name) # :nodoc: + column_name = column_name.to_s + columns(table_name).detect { |c| c.name == column_name } || + raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 23edc8b955..e5417a9556 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,18 +6,35 @@ module ActiveRecord include Savepoints class SchemaCreation < AbstractAdapter::SchemaCreation - def visit_AddColumn(o) add_column_position!(super, column_options(o)) end private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_TableDefinition(o) + name = o.name + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " + + statements = o.columns.map { |c| accept c } + statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) + + create_sql << "(#{statements.join(', ')}) " if statements.present? + create_sql << "#{o.options}" + create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql + end + def visit_ChangeColumnDefinition(o) column = o.column options = o.options sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" - add_column_options!(change_column_sql, options) + add_column_options!(change_column_sql, options.merge(column: column)) add_column_position!(change_column_sql, options) end @@ -29,6 +46,11 @@ module ActiveRecord end sql end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" + end end def schema_creation @@ -38,29 +60,25 @@ module ActiveRecord class Column < ConnectionAdapters::Column # :nodoc: attr_reader :collation, :strict, :extra - def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "") + def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") @strict = strict @collation = collation @extra = extra - super(name, default, sql_type, null) + super(name, default, cast_type, sql_type, null) + assert_valid_default(default) + extract_default end - def extract_default(default) + def extract_default if blob_or_text_column? - if default.blank? - null || strict ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) - nil - else - super + @default = null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(@default) + @default = nil end end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -68,54 +86,12 @@ module ActiveRecord sql_type =~ /blob/i || type == :text end - # Must return the relevant concrete adapter - def adapter - raise NotImplementedError - end - def case_sensitive? collation && !collation.match(/_ci$/) end private - def simplified_type(field_type) - return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end - end - - def extract_limit(sql_type) - case sql_type - when /^enum\((.+)\)/i - $1.split(',').map{|enum| enum.strip.length - 2}.max - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super - end - end - # MySQL misreports NOT NULL column default when none is given. # We can't detect this for columns which may have a legitimate '' # default (string) but we can for others (integer, datetime, boolean, @@ -126,6 +102,12 @@ module ActiveRecord def missing_default_forged_as_empty_string?(default) type != :string && !null && default == '' end + + def assert_valid_default(default) + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end end ## @@ -155,7 +137,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -165,21 +146,18 @@ module ActiveRecord INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - class BindSubstitution < Arel::Visitors::MySQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @visitor = Arel::Visitors::MySQL.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::MySQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -206,17 +184,6 @@ module ActiveRecord true end - def type_cast(value, column) - case value - when TrueClass - 1 - when FalseClass - 0 - else - super - end - end - # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -225,6 +192,14 @@ module ActiveRecord version[0] >= 5 end + def supports_indexes_in_create? + true + end + + def supports_foreign_keys? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -241,12 +216,11 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, extra) + def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -254,12 +228,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end @@ -277,10 +248,18 @@ module ActiveRecord QUOTED_TRUE end + def unquoted_true + 1 + end + def quoted_false QUOTED_FALSE end + def unquoted_false + 0 + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -296,6 +275,11 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def clear_cache! + super + reload_type_map + end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) log(sql, name) { @connection.query(sql) } @@ -405,7 +389,7 @@ module ActiveRecord end def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -449,7 +433,9 @@ module ActiveRecord execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) - new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) + sql_type = field[:Type] + cast_type = lookup_cast_type(sql_type) + new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -459,7 +445,7 @@ module ActiveRecord end def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| + sqls = operations.flat_map do |command, args| table, arguments = args.shift, args method = :"#{command}_sql" @@ -468,7 +454,7 @@ module ActiveRecord else raise "Unknown method called : #{method}(#{arguments.inspect})" end - end.flatten.join(", ") + end.join(", ") execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") end @@ -487,7 +473,7 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) - if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + if supports_rename_index? execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super @@ -523,6 +509,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -588,10 +602,19 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) + node = Arel::Nodes.build_quoted node, table_attribute Arel::Nodes::Bin.new(node) end + def case_sensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + table[attribute].eq(value) + else + super + end + end + def case_insensitive_comparison(table, attribute, column, value) if column.case_sensitive? super @@ -614,6 +637,34 @@ module ActiveRecord protected + def initialize_type_map(m) # :nodoc: + super + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + Type::String.new(limit: limit) + end + + m.register_type %r(tinytext)i, Type::Text.new(limit: 255) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) + m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) + m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) + m.register_type %r(^int)i, Type::Integer.new(limit: 4) + m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) + m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) + m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + m.register_type %r(^float)i, Type::Float.new(limit: 24) + m.register_type %r(^double)i, Type::Float.new(limit: 53) + + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans + m.alias_type %r(set)i, 'varchar' + m.alias_type %r(year)i, 'integer' + m.alias_type %r(bit)i, 'binary' + end + # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) @@ -683,15 +734,13 @@ module ActiveRecord end def rename_column_sql(table_name, column_name, new_column_name) - options = { name: new_column_name } - - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - options[:auto_increment] = (column.extra == "auto_increment") - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end + column = column_for(table_name, column_name) + options = { + name: new_column_name, + default: column.default, + null: column.null, + auto_increment: column.extra == "auto_increment" + } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] schema_creation.accept ChangeColumnDefinition.new column, current_type, options @@ -725,34 +774,39 @@ module ActiveRecord private + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def mariadb? + full_version =~ /mariadb/i + end + def supports_views? version[0] >= 5 end - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end def configure_connection - variables = @config[:variables] || {} + variables = @config.fetch(:variables, {}).stringify_keys # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variables[:sql_auto_is_null] = 0 + variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout) + variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?(:sql_mode) - variables[:sql_mode] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see @@ -773,6 +827,15 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index f2fbd5a8f2..5f9cc6edd0 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,101 +13,36 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function - attr_accessor :primary, :coder + attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function - alias :encoded? :coder + delegate :type, :precision, :scale, :limit, :klass, :accessor, + :number?, :binary?, :changed?, + :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, + :type_cast_for_schema, + to: :cast_type # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. + # +cast_type+ is the object used for type casting and type information. # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in # <tt>company_name varchar(60)</tt>. # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) + def initialize(name, default, cast_type, sql_type = nil, null = true) @name = name + @cast_type = cast_type @sql_type = sql_type @null = null - @limit = extract_limit(sql_type) - @precision = extract_precision(sql_type) - @scale = extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) + @default = default @default_function = nil - @primary = nil - @coder = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal end def has_default? !default.nil? end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :timestamp, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - def binary? - type == :binary - end - - # Casts a Ruby value to something appropriate for writing to the database. - def type_cast_for_write(value) - return value unless number? - - case value - when FalseClass - 0 - when TrueClass - 1 - when String - value.presence - else - value - end - end - - # Casts value (which is a String) to an appropriate instance. - def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text then value - when :integer then klass.value_to_integer(value) - when :float then value.to_f - 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.value_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value - end - end - # Returns the human name of the column name. # # ===== Examples @@ -116,177 +51,11 @@ module ActiveRecord Base.human_attribute_name(@name) end - def extract_default(default) - type_cast(default) - end - - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value + def with_type(type) + dup.tap do |clone| + clone.instance_variable_set('@cast_type', type) end - - 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) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - dummy_time_string = "2000-01-01 #{string}" - - fast_string_to_time(dummy_time_string) || begin - time_hash = Date._parse(dummy_time_string) - return nil if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.empty? - nil - else - TRUE_VALUES.include?(value) - end - end - - # Used to convert values to integer. - # handle the case when an integer column is used to store boolean values - def value_to_integer(value) - case value - when TrueClass, FalseClass - value ? 1 : 0 - else - value.to_i rescue nil - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return nil unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i - :string - when /boolean/i - :boolean - end - end end end # :startdoc: diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 3f8b14bf67..d28a54b8f9 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -32,10 +32,15 @@ module ActiveRecord # } def initialize(url) raise "Database URL cannot be empty" if url.blank? - @uri = URI.parse(url) - @adapter = @uri.scheme + @uri = uri_parser.parse(url) + @adapter = @uri.scheme.gsub('-', '_') @adapter = "postgresql" if @adapter == "postgres" - @query = @uri.query || '' + + if @uri.opaque + @uri.opaque, @query = @uri.opaque.split('?', 2) + else + @query = @uri.query + end end # Converts the given URL to a full connection hash. @@ -57,38 +62,46 @@ module ActiveRecord # Converts the query parameters of the URI into a hash. # - # "localhost?pool=5&reap_frequency=2" - # # => { "pool" => "5", "reap_frequency" => "2" } + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } # # returns empty hash if no query present. # # "localhost" # # => {} def query_hash - Hash[@query.split("&").map { |pair| pair.split("=") }] + Hash[(@query || '').split("&").map { |pair| pair.split("=") }] end def raw_config - query_hash.merge({ - "adapter" => @adapter, - "username" => uri.user, - "password" => uri.password, - "port" => uri.port, - "database" => database, - "host" => uri.host }) + if uri.opaque + query_hash.merge({ + "adapter" => @adapter, + "database" => uri.opaque }) + else + query_hash.merge({ + "adapter" => @adapter, + "username" => uri.user, + "password" => uri.password, + "port" => uri.port, + "database" => database_from_path, + "host" => uri.hostname }) + end end # Returns name of the database. - # Sqlite3 expects this to be a full path or `:memory:`. - def database + def database_from_path if @adapter == 'sqlite3' - if '/:memory:' == uri.path - ':memory:' - else - uri.path - end + # 'sqlite3:/foo' is absolute, because that makes sense. The + # corresponding relative version, 'sqlite3:foo', is handled + # elsewhere, as an "opaque". + + uri.path else - uri.path.sub(%r{^/},"") + # Only SQLite uses a filename as the "database" name; for + # anything else, a leading slash would be silly. + + uri.path.sub(%r{^/}, "") end end end @@ -124,7 +137,7 @@ module ActiveRecord if config resolve_connection config elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call - resolve_env_connection env.to_sym + resolve_symbol_connection env.to_sym else raise AdapterNotSpecified end @@ -147,7 +160,7 @@ module ActiveRecord # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } # spec = Resolver.new(config).spec(:production) # spec.adapter_method - # # => "sqlite3" + # # => "sqlite3_connection" # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # @@ -193,42 +206,41 @@ module ActiveRecord # def resolve_connection(spec) case spec - when Symbol, String - resolve_env_connection spec + when Symbol + resolve_symbol_connection spec + when String + resolve_string_connection spec when Hash resolve_hash_connection spec end end + def resolve_string_connection(spec) + # Rails has historically accepted a string to mean either + # an environment key or a URL spec, so we have deprecated + # this ambiguous behaviour and in the future this function + # can be removed in favor of resolve_url_connection. + if configurations.key?(spec) || spec !~ /:/ + ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ + "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" + resolve_symbol_connection(spec) + else + resolve_url_connection(spec) + end + end + # Takes the environment such as `:production` or `:development`. # This requires that the @configurations was initialized with a key that # matches. # - # - # Resolver.new("production" => {}).resolve_env_connection(:production) + # Resolver.new("production" => {}).resolve_symbol_connection(:production) # # => {} # - # Takes a connection URL. - # - # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo") - # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } - # - def resolve_env_connection(spec) - # Rails has historically accepted a string to mean either - # an environment key or a URL spec, so we have deprecated - # this ambiguous behaviour and in the future this function - # can be removed in favor of resolve_string_connection and - # resolve_symbol_connection. + def resolve_symbol_connection(spec) if config = configurations[spec.to_s] - if spec.is_a?(String) - ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ - "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" - end resolve_connection(config) - elsif spec.is_a?(String) - resolve_string_connection(spec) else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available configuration: #{configurations.inspect}") + raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") end end @@ -237,14 +249,19 @@ module ActiveRecord # hash and merges with the rest of the hash. # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) - if url = spec.delete("url") - connection_hash = resolve_string_connection(url) + if spec["url"] && spec["url"] !~ /^jdbc:/ + connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end spec end - def resolve_string_connection(url) + # Takes a connection URL. + # + # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_url_connection(url) ConnectionUrlResolver.new(url).to_hash 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 b07b0cb826..39d52e6349 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -20,27 +20,20 @@ module ActiveRecord ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end end module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column # :nodoc: - def adapter - Mysql2Adapter - end - end - ADAPTER_NAME = 'Mysql2' def initialize(connection, logger, connection_options, config) super - @visitor = BindSubstitution.new self + @prepared_statements = false configure_connection end @@ -69,10 +62,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) exception.error_number if exception.respond_to?(:error_number) end @@ -83,6 +72,14 @@ module ActiveRecord @connection.escape(string) end + def quoted_date(value) + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + # CONNECTION MANAGEMENT ==================================== def active? @@ -272,8 +269,8 @@ module ActiveRecord super end - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + def full_version + @full_version ||= @connection.info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 49f0bfbcde..a03bc28744 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -36,9 +36,9 @@ module ActiveRecord ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) rescue Mysql::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end end @@ -66,35 +66,6 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column #:nodoc: - def self.string_to_time(value) - return super unless Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - end - - def self.string_to_dummy_time(v) - return super unless Mysql::Time === v - new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) - end - - def self.string_to_date(v) - return super unless Mysql::Time === v - new_date(v.year, v.month, v.day) - end - - def adapter - MysqlAdapter - end - end - ADAPTER_NAME = 'MySQL' class StatementPool < ConnectionAdapters::StatementPool @@ -156,10 +127,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -222,6 +189,7 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! + super @statements.clear end @@ -294,126 +262,70 @@ module ActiveRecord @connection.insert_id end - module Fields - class Type - def type; end - - def type_cast_for_write(value) - value - end - end - - class Identity < Type - def type_cast(value); value; end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - value.to_i rescue value ? 1 : 0 - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_time value - end - end - - class Time < Type - def type; :time; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type; :float; end - - def type_cast(value) - return if value.nil? - - value.to_f + module Fields # :nodoc: + class DateTime < Type::DateTime # :nodoc: + def cast_value(value) + if Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value + class Time < Type::Time # :nodoc: + def cast_value(value) + if Mysql::Time === value + new_time( + 2000, + 01, + 01, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Boolean < Type - def type_cast(value) - return if value.nil? + class << self + TYPES = Type::HashLookupTypeMap.new # :nodoc: - ConnectionAdapters::Column.value_to_boolean value - end - end - - TYPES = {} - - # Register an MySQL +type_id+ with a typecasting object in - # +type+. - def self.register_type(type_id, type) - TYPES[type_id] = type - end + delegate :register_type, :alias_type, to: :TYPES - def self.alias_type(new, old) - TYPES[new] = TYPES[old] - end - - def self.find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES[Mysql::Field::TYPE_LONG] - else - TYPES.fetch(field.type) { Fields::Identity.new } + def find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES.lookup(Mysql::Field::TYPE_LONG) + else + TYPES.lookup(field.type) + end end end - register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new - register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + register_type Mysql::Field::TYPE_TINY, Type::Boolean.new + register_type Mysql::Field::TYPE_LONG, Type::Integer.new alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new - register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new - register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATE, Type::Date.new register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + register_type Mysql::Field::TYPE_FLOAT, Type::Float.new + end - Mysql::Field.constants.grep(/TYPE/).map { |class_name| - Mysql::Field.const_get class_name - }.reject { |const| TYPES.key? const }.each do |const| - register_type const, Fields::Identity.new - end + def initialize_type_map(m) # :nodoc: + super + m.register_type %r(datetime)i, Fields::DateTime.new + m.register_type %r(time)i, Fields::Time.new end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -431,7 +343,7 @@ module ActiveRecord fields << field_name if field.decimals > 0 - types[field_name] = Fields::Decimal.new + types[field_name] = Type::Decimal.new else types[field_name] = Fields.find_type field end @@ -447,7 +359,7 @@ module ActiveRecord end end - def execute_and_free(sql, name = nil) + def execute_and_free(sql, name = nil) # :nodoc: result = execute(sql, name) ret = yield result result.free @@ -460,7 +372,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_delete(sql, name, binds) + def exec_delete(sql, name, binds) # :nodoc: affected_rows = 0 exec_query(sql, name, binds) do |n| @@ -497,7 +409,7 @@ module ActiveRecord stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close @@ -558,9 +470,9 @@ module ActiveRecord rows end - # Returns the version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + # Returns the full version of the connected MySQL server. + def full_version + @full_version ||= @connection.server_info end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index 0b218f2bfd..1b74c039ce 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLColumn < Column - module ArrayParser + module PostgreSQL + module ArrayParser # :nodoc: DOUBLE_QUOTE = '"' BACKSLASH = "\\" @@ -9,35 +9,23 @@ module ActiveRecord BRACKET_OPEN = '{' BRACKET_CLOSE = '}' - private - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - def parse_pg_array(string) - parse_data(string) + def parse_pg_array(string) # :nodoc: + local_index = 0 + array = [] + while(local_index < string.length) + case string[local_index] + when BRACKET_OPEN + local_index,array = parse_array_contents(array, string, local_index + 1) + when BRACKET_CLOSE + return array end + local_index += 1 end - def parse_data(string) - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end + array + end - array - end + private def parse_array_contents(array, string, index) is_escaping = false diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index 35ce881302..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,164 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - class PostgreSQLColumn < Column - module Cast - def point_to_string(point) - "(#{point[0]},#{point[1]})" - end - - def string_to_point(string) - if string[0] == '(' && string[-1] == ')' - string = string[1...-1] - end - string.split(',').map{ |v| Float(v) } - end - - def string_to_time(string) - return string unless String === string - - case string - when 'infinity'; Float::INFINITY - when '-infinity'; -Float::INFINITY - when / BC$/ - super("-" + string.sub(/ BC$/, "")) - else - super - end - end - - def string_to_bit(value) - case value - when /^0x/i - value[2..-1].hex.to_s(2) # Hexadecimal notation - else - value # Bit-string notation - end - end - - def hstore_to_string(object) - if Hash === object - object.map { |k,v| - "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ',' - else - object - end - end - - def string_to_hstore(string) - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k,v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k,v] - }] - else - string - end - end - - def json_to_string(object) - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def array_to_string(value, column, adapter) - casted_values = value.map do |val| - if String === val - if val == "NULL" - "\"#{val}\"" - else - quote_and_escape(adapter.type_cast(val, column, true)) - end - else - adapter.type_cast(val, column, true) - end - end - "{#{casted_values.join(',')}}" - end - - def range_to_string(object) - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - def string_to_cidr(string) - if string.nil? - nil - elsif String === string - begin - IPAddr.new(string) - rescue ArgumentError - nil - end - else - string - end - end - - def cidr_to_string(object) - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - end - end - - def string_to_array(string, oid) - parse_pg_array(string).map {|val| type_cast_array(oid, val)} - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - - def quote_and_escape(value) - case value - when "NULL", Numeric - value - else - "\"#{value.gsub(/"/,"\\\"")}\"" - end - end - - def type_cast_array(oid, value) - if ::Array === value - value.map {|item| type_cast_array(oid, item)} - else - oid.type_cast value - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb new file mode 100644 index 0000000000..37e5c3859c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + # PostgreSQL-specific extensions to column definitions in a table. + class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array + + def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) + if sql_type =~ /\[\]$/ + @array = true + super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, default, cast_type, sql_type, null) + end + + @default_function = default_function + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 51ee2829b2..89a7257d77 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" @@ -44,10 +44,32 @@ module ActiveRecord end end + def select_value(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 + end + end + + def select_values(arel, name = nil) + arel, binds = binds_from_relation arel, [] + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + if result.nfields > 0 + result.column_values(0) + else + [] + end + end + end + # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + execute_and_clear(sql, name, binds) do |result| + result.values + end end # Executes an INSERT query and returns the new record's ID @@ -72,6 +94,11 @@ module ActiveRecord super.insert end + # The internal PostgreSQL identifier of the money data type. + MONEY_COLUMN_TYPE_OID = 790 #:nodoc: + # The internal PostgreSQL identifier of the BYTEA data type. + BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: + # create a 2D array representing the result set def result_as_array(res) #:nodoc: # check if we have any binary column and if they need escaping @@ -134,31 +161,20 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - - types = {} - fields = result.fields - fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = type_map.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } + execute_and_clear(sql, name, binds) do |result| + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = get_oid_type(ftype, fmod, fname) + end + ActiveRecord::Result.new(fields, result.values, types) end - - ret = ActiveRecord::Result.new(fields, result.values, types) - result.clear - return ret end def exec_delete(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - affected = result.cmd_tuples - result.clear - affected + execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } end alias :exec_update :exec_delete diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index fae260a921..d28a2b4fa0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,380 +1,35 @@ -require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/postgresql/oid/infinity' + +require 'active_record/connection_adapters/postgresql/oid/array' +require 'active_record/connection_adapters/postgresql/oid/bit' +require 'active_record/connection_adapters/postgresql/oid/bit_varying' +require 'active_record/connection_adapters/postgresql/oid/bytea' +require 'active_record/connection_adapters/postgresql/oid/cidr' +require 'active_record/connection_adapters/postgresql/oid/date' +require 'active_record/connection_adapters/postgresql/oid/date_time' +require 'active_record/connection_adapters/postgresql/oid/decimal' +require 'active_record/connection_adapters/postgresql/oid/enum' +require 'active_record/connection_adapters/postgresql/oid/float' +require 'active_record/connection_adapters/postgresql/oid/hstore' +require 'active_record/connection_adapters/postgresql/oid/inet' +require 'active_record/connection_adapters/postgresql/oid/integer' +require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/jsonb' +require 'active_record/connection_adapters/postgresql/oid/money' +require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/range' +require 'active_record/connection_adapters/postgresql/oid/specialized_string' +require 'active_record/connection_adapters/postgresql/oid/time' +require 'active_record/connection_adapters/postgresql/oid/uuid' +require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' + +require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module OID - class Type - def type; end - end - - class Identity < Type - def type_cast(value) - value - end - end - - class Bit < Type - def type_cast(value) - if String === value - ConnectionAdapters::PostgreSQLColumn.string_to_bit value - else - value - end - end - end - - class Bytea < Type - def type_cast(value) - return if value.nil? - PGconn.unescape_bytea value - end - end - - class Money < Type - def type_cast(value) - return if value.nil? - return value unless String === value - - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - # Negative values are represented as follows: - # (3) -$2.55 - # (4) ($2.55) - - value.sub!(/^\((.+)\)$/, '-\1') # (4) - case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - value.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Vector < Type - attr_reader :delim, :subtype - - # +delim+ corresponds to the `typdelim` column in the pg_types - # table. +subtype+ is derived from the `typelem` column in the - # pg_types table. - def initialize(delim, subtype) - @delim = delim - @subtype = subtype - end - - # FIXME: this should probably split on +delim+ and use +subtype+ - # to cast the values. Unfortunately, the current Rails behavior - # is to just return the string. - def type_cast(value) - value - end - end - - class Point < Type - def type_cast(value) - if String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value - else - value - end - end - end - - class Array < Type - attr_reader :subtype - def initialize(subtype) - @subtype = subtype - end - - def type_cast(value) - if String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype - else - value - end - end - end - - class Range < Type - attr_reader :subtype - def initialize(subtype) - @subtype = subtype - end - - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from, - to: (value[-2] == ',' || to == 'infinity') ? infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity(options = {}) - ::Float::INFINITY * (options[:negative] ? -1 : 1) - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - - def to_integer(value) - infinity?(value) ? value : value.to_i - end - - def type_cast(value) - return if value.nil? || value == 'empty' - return value if value.is_a?(::Range) - - extracted = extract_bounds(value) - - case @subtype - when :date - from = ConnectionAdapters::Column.value_to_date(extracted[:from]) - from -= 1.day if extracted[:exclude_start] - to = ConnectionAdapters::Column.value_to_date(extracted[:to]) - when :decimal - from = BigDecimal.new(extracted[:from].to_s) - # FIXME: add exclude start for ::Range, same for timestamp ranges - to = BigDecimal.new(extracted[:to].to_s) - when :time - from = ConnectionAdapters::Column.string_to_time(extracted[:from]) - to = ConnectionAdapters::Column.string_to_time(extracted[:to]) - when :integer - from = to_integer(extracted[:from]) rescue value ? 1 : 0 - from -= 1 if extracted[:exclude_start] - to = to_integer(extracted[:to]) rescue value ? 1 : 0 - else - return value - end - - ::Range.new(from, to, extracted[:exclude_end]) - end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end - end - - class Boolean < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - class Timestamp < Type - def type; :timestamp; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::PostgreSQLColumn.string_to_time value - end - end - - class Date < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class Time < Type - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type_cast(value) - return if value.nil? - - value.to_f - end - end - - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Hstore < Type - def type_cast_for_write(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string value - end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_hstore value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Cidr < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_cidr value - end - end - - class Json < Type - def type_cast_for_write(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string value - end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_json value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class TypeMap - def initialize - @mapping = {} - end - - def []=(oid, type) - @mapping[oid] = type - end - - def [](oid) - @mapping[oid] - end - - def clear - @mapping.clear - end - - def key?(oid) - @mapping.key? oid - end - - def fetch(ftype, fmod) - # The type for the numeric depends on the width of the field, - # so we'll do something special here. - # - # When dealing with decimal columns: - # - # places after decimal = fmod - 4 & 0xffff - # places before decimal = (fmod - 4) >> 16 & 0xffff - if ftype == 1700 && (fmod - 4 & 0xffff).zero? - ftype = 23 - end - - @mapping.fetch(ftype) { |oid| yield oid, fmod } - end - end - - # When the PG adapter connects, the pg_type table is queried. The - # key of this hash maps to the `typname` column from the table. - # type_map is then dynamically built with oids as the key and type - # objects as values. - NAMES = Hash.new { |h,k| # :nodoc: - h[k] = OID::Identity.new - } - - # Register an OID type named +name+ with a typecasting object in - # +type+. +name+ should correspond to the `typname` column in - # the `pg_type` table. - def self.register_type(name, type) - NAMES[name] = type - end - - # Alias the +old+ type to the +new+ type. - def self.alias_type(new, old) - NAMES[new] = NAMES[old] - end - - # Is +name+ a registered type? - def self.registered_type?(name) - NAMES.key? name - end - - register_type 'int2', OID::Integer.new - alias_type 'int4', 'int2' - alias_type 'int8', 'int2' - alias_type 'oid', 'int2' - - register_type 'daterange', OID::Range.new(:date) - register_type 'numrange', OID::Range.new(:decimal) - register_type 'tsrange', OID::Range.new(:time) - register_type 'int4range', OID::Range.new(:integer) - alias_type 'tstzrange', 'tsrange' - alias_type 'int8range', 'int4range' - - register_type 'numeric', OID::Decimal.new - register_type 'text', OID::Identity.new - alias_type 'varchar', 'text' - alias_type 'char', 'text' - alias_type 'bpchar', 'text' - alias_type 'xml', 'text' - - # FIXME: why are we keeping these types as strings? - alias_type 'tsvector', 'text' - alias_type 'interval', 'text' - alias_type 'macaddr', 'text' - alias_type 'uuid', 'text' - - register_type 'money', OID::Money.new - register_type 'bytea', OID::Bytea.new - register_type 'bool', OID::Boolean.new - register_type 'bit', OID::Bit.new - register_type 'varbit', OID::Bit.new - - register_type 'float4', OID::Float.new - alias_type 'float8', 'float4' - - register_type 'timestamp', OID::Timestamp.new - register_type 'timestamptz', OID::Timestamp.new - register_type 'date', OID::Date.new - register_type 'time', OID::Time.new - - register_type 'path', OID::Identity.new - register_type 'point', OID::Point.new - register_type 'polygon', OID::Identity.new - register_type 'circle', OID::Identity.new - register_type 'hstore', OID::Hstore.new - register_type 'json', OID::Json.new - register_type 'ltree', OID::Identity.new - - register_type 'cidr', OID::Cidr.new - alias_type 'inet', 'cidr' + module PostgreSQL + module OID # :nodoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb new file mode 100644 index 0000000000..cd5efe2bb8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Array < Type::Value # :nodoc: + include Type::Mutable + + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + + attr_reader :subtype, :delimiter + delegate :type, to: :subtype + + def initialize(subtype, delimiter = ',') + @subtype = subtype + @delimiter = delimiter + end + + def type_cast_from_database(value) + if value.is_a?(::String) + type_cast_array(parse_pg_array(value), :type_cast_from_database) + else + super + end + end + + def type_cast_from_user(value) + type_cast_array(value, :type_cast_from_user) + end + + def type_cast_for_database(value) + if value.is_a?(::Array) + cast_value_for_database(value) + else + super + end + end + + private + + def type_cast_array(value, method) + if value.is_a?(::Array) + value.map { |item| type_cast_array(item, method) } + else + @subtype.public_send(method, value) + end + end + + def cast_value_for_database(value) + if value.is_a?(::Array) + casted_values = value.map { |item| cast_value_for_database(item) } + "{#{casted_values.join(delimiter)}}" + else + quote_and_escape(subtype.type_cast_for_database(value)) + end + end + + ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays + + def quote_and_escape(value) + case value + when ::String + if string_requires_quoting?(value) + value = value.gsub(/\\/, ARRAY_ESCAPE) + value.gsub!(/"/,"\\\"") + %("#{value}") + else + value + end + when nil then "NULL" + else value + end + end + + # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO + # for a list of all cases in which strings will be quoted. + def string_requires_quoting?(string) + string.empty? || + string == "NULL" || + string =~ /[\{\}"\\\s]/ || + string.include?(delimiter) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb new file mode 100644 index 0000000000..1dbb40ca1d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -0,0 +1,52 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bit < Type::Value # :nodoc: + def type + :bit + end + + def type_cast(value) + if ::String === value + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + else + value + end + end + + def type_cast_for_database(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb new file mode 100644 index 0000000000..4c21097d48 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class BitVarying < OID::Bit # :nodoc: + def type + :bit_varying + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb new file mode 100644 index 0000000000..997613d7be --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -0,0 +1,14 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bytea < Type::Binary # :nodoc: + def type_cast_from_database(value) + return if value.nil? + PGconn.unescape_bytea(super) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb new file mode 100644 index 0000000000..a53b4ee8e2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Cidr < Type::Value # :nodoc: + def type + :cidr + end + + def type_cast_for_schema(value) + subnet_mask = value.instance_variable_get(:@mask_addr) + + # If the subnet mask is equal to /32, don't output it + if subnet_mask == (2**32 - 1) + "\"#{value.to_s}\"" + else + "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + end + end + + def type_cast_for_database(value) + if IPAddr === value + "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + value + end + end + + def cast_value(value) + if value.nil? + nil + elsif String === value + begin + IPAddr.new(value) + rescue ArgumentError + nil + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb new file mode 100644 index 0000000000..1d8d264530 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb new file mode 100644 index 0000000000..b9e7894e5c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class DateTime < Type::DateTime # :nodoc: + include Infinity + + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) + else + super + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb new file mode 100644 index 0000000000..43d22c8daf --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Decimal < Type::Decimal # :nodoc: + def infinity(options = {}) + BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb new file mode 100644 index 0000000000..77d5038efd --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Enum < Type::Value # :nodoc: + def type + :enum + end + + def type_cast(value) + value.to_s + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb new file mode 100644 index 0000000000..78ef94b912 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Float < Type::Float # :nodoc: + include Infinity + + def cast_value(value) + case value + when ::Float then value + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::Float::NAN + else value.to_f + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb new file mode 100644 index 0000000000..be4525c94f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -0,0 +1,59 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Hstore < Type::Value # :nodoc: + include Type::Mutable + + def type + :hstore + end + + def type_cast_from_database(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end + end + + def type_cast_for_database(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb new file mode 100644 index 0000000000..96486fa65b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Inet < Cidr # :nodoc: + def type + :inet + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb new file mode 100644 index 0000000000..e47780399a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + module Infinity # :nodoc: + def infinity(options = {}) + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb new file mode 100644 index 0000000000..59abdc0009 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Integer < Type::Integer # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb new file mode 100644 index 0000000000..e12ddd9901 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Json < Type::Value # :nodoc: + include Type::Mutable + + def type + :json + end + + def type_cast_from_database(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) + else + super + end + end + + def type_cast_for_database(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + super + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb new file mode 100644 index 0000000000..34ed32ad35 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Jsonb < Json # :nodoc: + def type + :jsonb + end + + def changed_in_place?(raw_old_value, new_value) + # Postgres does not preserve insignificant whitespaces when + # roundtripping jsonb columns. This causes some false positives for + # the comparison here. Therefore, we need to parse and re-dump the + # raw value here to ensure the insignificant whitespaces are + # consitent with our encoder's output. + raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + super(raw_old_value, new_value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb new file mode 100644 index 0000000000..df890c2ed6 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Money < Type::Decimal # :nodoc: + include Infinity + + class_attribute :precision + + def type + :money + end + + def scale + 2 + end + + def cast_value(value) + return value unless ::String === value + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + + value.sub!(/^\((.+)\)$/, '-\1') # (4) + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + super(value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb new file mode 100644 index 0000000000..bac8b01d6b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Point < Type::Value # :nodoc: + include Type::Mutable + + def type + :point + end + + def type_cast(value) + case value + when ::String + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + type_cast(value.split(',')) + when ::Array + value.map { |v| Float(v) } + else + value + end + end + + def type_cast_for_database(value) + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb new file mode 100644 index 0000000000..ae967d5167 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -0,0 +1,76 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Range < Type::Value # :nodoc: + attr_reader :subtype, :type + + def initialize(subtype, type) + @subtype = subtype + @type = type + end + + def type_cast_for_schema(value) + value.inspect.gsub('Infinity', '::Float::INFINITY') + end + + def cast_value(value) + return if value == 'empty' + return value if value.is_a?(::Range) + + extracted = extract_bounds(value) + from = type_cast_single extracted[:from] + to = type_cast_single extracted[:to] + + if !infinity?(from) && extracted[:exclude_start] + if from.respond_to?(:succ) + from = from.succ + ActiveSupport::Deprecation.warn <<-MESSAGE +Excluding the beginning of a Range is only partialy supported through `#succ`. +This is not reliable and will be removed in the future. + MESSAGE + else + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" + end + end + ::Range.new(from, to, extracted[:exclude_end]) + end + + def type_cast_for_database(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.type_cast_for_database(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb new file mode 100644 index 0000000000..2d2fede4e8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class SpecializedString < Type::String # :nodoc: + attr_reader :type + + def initialize(type) + @type = type + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb new file mode 100644 index 0000000000..8f0246eddb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Time < Type::Time # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb new file mode 100644 index 0000000000..e396ff4a1e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -0,0 +1,85 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + # This class uses the data from PostgreSQL pg_type table to build + # the OID -> Type mapping. + # - OID is and integer representing the type. + # - Type is an OID::Type object. + # This class has side effects on the +store+ passed during initialization. + class TypeMapInitializer # :nodoc: + def initialize(store) + @store = store + end + + def run(records) + nodes = records.reject { |row| @store.key? row['oid'].to_i } + mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + + mapped.each { |row| register_mapped_type(row) } + enums.each { |row| register_enum_type(row) } + domains.each { |row| register_domain_type(row) } + arrays.each { |row| register_array_type(row) } + ranges.each { |row| register_range_type(row) } + composites.each { |row| register_composite_type(row) } + end + + private + def register_mapped_type(row) + alias_type row['oid'], row['typname'] + end + + def register_enum_type(row) + register row['oid'], OID::Enum.new + end + + def register_array_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Array.new(subtype, row['typdelim']) + end + end + + def register_range_type(row) + if subtype = @store.lookup(row['rngsubtype'].to_i) + register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + end + end + + def register_domain_type(row) + if base_type = @store.lookup(row["typbasetype"].to_i) + register row['oid'], base_type + else + warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + end + end + + def register_composite_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Vector.new(row['typdelim'], subtype) + end + end + + def register(oid, oid_type) + oid = assert_valid_registration(oid, oid_type) + @store.register_type(oid, oid_type) + end + + def alias_type(oid, target) + oid = assert_valid_registration(oid, target) + @store.alias_type(oid, target) + end + + def assert_valid_registration(oid, oid_type) + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + oid.to_i + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb new file mode 100644 index 0000000000..dd97393eac --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Uuid < Type::Value # :nodoc: + RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [1-5][a-fA-F0-9]{3}-? + [8-Bab][a-fA-F0-9]{3}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-?\}?\z}x + + def type + :uuid + end + + def type_cast(value) + value.to_s[RFC_4122, 0] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb new file mode 100644 index 0000000000..de4187b028 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Vector < Type::Value # :nodoc: + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..334af7c598 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def type_cast_for_database(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c1f978a081..cf5c8d288e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -1,141 +1,35 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module Quoting # Escapes binary strings for bytea input to the database. def escape_bytea(value) - PGconn.escape_bytea(value) if value + @connection.escape_bytea(value) if value end # Unescapes bytea output from a database to the binary string it represents. # NOTE: This is NOT an inverse of escape_bytea! This is only to be used # on escaped binary output from database drive. def unescape_bytea(value) - PGconn.unescape_bytea(value) if value + @connection.unescape_bytea(value) if value end # Quotes PostgreSQL-specific data types for SQL input. def quote(value, column = nil) #:nodoc: return super unless column - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" - else - super - end - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end - when IPAddr - case sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? + if value.infinite? || value.nan? "'#{value.to_s}'" else super end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'bytea' then "'#{escape_bytea(value)}'" - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end else super end end - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - if column.array - PostgreSQLColumn.array_to_string(value, column, self) - else - super(value, column) - end - end - when String - if 'bytea' == column.sql_type - # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc - # for more information - { value: value, format: 1 } - else - super(value, column) - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - when IPAddr - if %w(inet cidr).include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -150,14 +44,7 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end + Utils.extract_schema_qualified_name(name.to_s).quoted end def quote_table_name_for_assignment(table, attr) @@ -177,11 +64,54 @@ module ActiveRecord result = "#{result}.#{sprintf("%06d", value.usec)}" end - if value.year < 0 - result = result.sub(/^-/, "") + " BC" + if value.year <= 0 + bce_year = format("%04d", -value.year + 1) + result = result.sub(/^-?\d+/, bce_year) + " BC" end result end + + # Does not quote function default values for UUID columns + def quote_default_value(value, column) #:nodoc: + if column.type == :uuid && value =~ /\(\)/ + value + else + quote(value, column) + end + end + + private + + def _quote(value) + case value + when Type::Binary::Data + "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end + else + super + end + end + + def _type_cast(value) + case value + when Type::Binary::Data + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s + else + super + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index bc775394a6..52b307c432 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -1,12 +1,12 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module ReferentialIntegrity - def supports_disable_referential_integrity? #:nodoc: + module PostgreSQL + module ReferentialIntegrity # :nodoc: + def supports_disable_referential_integrity? # :nodoc: true end - def disable_referential_integrity #:nodoc: + def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? begin execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb new file mode 100644 index 0000000000..83554bbf74 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,154 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnMethods + def xml(*args) + options = args.extract_options! + column(args[0], :xml, options) + end + + def tsvector(*args) + options = args.extract_options! + column(args[0], :tsvector, options) + end + + def int4range(name, options = {}) + column(name, :int4range, options) + end + + def int8range(name, options = {}) + column(name, :int8range, options) + end + + def tsrange(name, options = {}) + column(name, :tsrange, options) + end + + def tstzrange(name, options = {}) + column(name, :tstzrange, options) + end + + def numrange(name, options = {}) + column(name, :numrange, options) + end + + def daterange(name, options = {}) + column(name, :daterange, options) + end + + def hstore(name, options = {}) + column(name, :hstore, options) + end + + def ltree(name, options = {}) + column(name, :ltree, options) + end + + def inet(name, options = {}) + column(name, :inet, options) + end + + def cidr(name, options = {}) + column(name, :cidr, options) + end + + def macaddr(name, options = {}) + column(name, :macaddr, options) + end + + def uuid(name, options = {}) + column(name, :uuid, options) + end + + def json(name, options = {}) + column(name, :json, options) + end + + def jsonb(name, options = {}) + column(name, :jsonb, options) + end + + def citext(name, options = {}) + column(name, :citext, options) + end + + def point(name, options = {}) + column(name, :point, options) + end + + def bit(name, options) + column(name, :bit, options) + end + + def bit_varying(name, options) + column(name, :bit_varying, options) + end + + def money(name, options) + column(name, :money, options) + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] = options.fetch(:default, 'uuid_generate_v4()') + options[:primary_key] = true + column name, type, options + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def create_column_definition(name, type) + PostgreSQL::ColumnDefinition.new name, type + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index ae8ede4b42..7042817672 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,18 +1,18 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation private def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" add_column_options!(sql, column_options(o)) end def visit_ColumnDefinition(o) sql = super - if o.primary_key? && o.type == :uuid + if o.primary_key? && o.type != :primary_key sql << " PRIMARY KEY " add_column_options!(sql, column_options(o)) end @@ -31,10 +31,14 @@ module ActiveRecord super end end - end - def schema_creation - SchemaCreation.new self + def type_for_column(column) + if column.array + @conn.lookup_cast_type("#{column.sql_type}[]") + else + super + end + end end module SchemaStatements @@ -101,19 +105,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table - - binds = [[nil, table]] - binds << [nil, schema] if schema + name = Utils.extract_schema_qualified_name(name.to_s) + return false unless name.identifier exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' - AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end @@ -185,13 +186,17 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = type_map.fetch(oid.to_i, fmod.to_i) { - OID::Identity.new - } - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) + default_value = extract_value_from_default(oid, default) + default_function = extract_default_function(default_value, default) + new_column(column_name, default_value, oid, type, notnull == 'f', default_function) end end + def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) + end + # Returns the current database name. def current_database query('select current_database()', 'SCHEMA')[0][0] @@ -276,9 +281,9 @@ module ActiveRecord def default_sequence_name(table_name, pk = nil) #:nodoc: result = serial_sequence(table_name, pk || 'id') return nil unless result - result.split('.').last + Utils.extract_schema_qualified_name(result) rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" + PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq") end def serial_sequence(table, column) @@ -315,24 +320,27 @@ module ActiveRecord # First try looking for a sequence with a dependency on the # given table's primary key. result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, seq.relname + SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, pg_depend dep, - pg_constraint cons + pg_constraint cons, + pg_namespace nsp WHERE seq.oid = dep.objid AND seq.relkind = 'S' AND attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid AND attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + AND seq.relnamespace = nsp.oid AND cons.contype = 'p' + AND dep.classid = 'pg_class'::regclass AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql if result.nil? or result.empty? result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, + SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN @@ -344,13 +352,19 @@ module ActiveRecord JOIN pg_attribute attr ON (t.oid = attrelid) JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end - [result.first, result.last] + pk = result.shift + if result.last + [pk, PostgreSQL::Name.new(*result)] + else + [pk, nil] + end rescue nil end @@ -369,8 +383,8 @@ module ActiveRecord end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -378,7 +392,7 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end @@ -408,13 +422,24 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default(table_name, column_name, default) clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + column = column_for(table_name, column_name) + return unless column + + alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + execute alter_column_query % "DROP DEFAULT" + else + execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + end end def change_column_null(table_name, column_name, null, default = nil) clear_cache! unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + column = column_for(table_name, column_name) + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end @@ -439,6 +464,42 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end @@ -488,7 +549,7 @@ module ActiveRecord # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } [super, *order_columns].join(', ') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb new file mode 100644 index 0000000000..0290bcb48c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -0,0 +1,66 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + # Value Object to hold a schema qualified name. + # This is usually the name of a PostgreSQL relation but it can also represent + # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent + # double quoting. + class Name # :nodoc: + SEPARATOR = "." + attr_reader :schema, :identifier + + def initialize(schema, identifier) + @schema, @identifier = unquote(schema), unquote(identifier) + end + + def to_s + parts.join SEPARATOR + end + + def quoted + parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + end + + def ==(o) + o.class == self.class && o.parts == parts + end + alias_method :eql?, :== + + def hash + parts.hash + end + + protected + def unquote(part) + return unless part + part.gsub(/(^"|"$)/,'') + end + + def parts + @parts ||= [@schema, @identifier].compact + end + end + + module Utils # :nodoc: + extend self + + # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> + # extracted from +string+. + # +schema+ is nil if not specified in +string+. + # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) + # +string+ supports the range of schema/table references understood by PostgreSQL, for example: + # + # * <tt>table_name</tt> + # * <tt>"table.name"</tt> + # * <tt>schema_name.table_name</tt> + # * <tt>schema_name."table.name"</tt> + # * <tt>"schema_name".table_name</tt> + # * <tt>"schema.name"."table name"</tt> + def extract_schema_qualified_name(string) + table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + PostgreSQL::Name.new(schema, table) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 36c7462419..eede374678 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,12 +1,15 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' + +require 'active_record/connection_adapters/postgresql/utils' +require 'active_record/connection_adapters/postgresql/column' require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/cast' -require 'active_record/connection_adapters/postgresql/array_parser' require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/referential_integrity' +require 'active_record/connection_adapters/postgresql/schema_definitions' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' -require 'active_record/connection_adapters/postgresql/referential_integrity' + require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -43,222 +46,6 @@ module ActiveRecord end module ConnectionAdapters - # PostgreSQL-specific extensions to column definitions in a table. - class PostgreSQLColumn < Column #:nodoc: - attr_accessor :array - - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type - default_value = self.class.extract_value_from_default(default) - - if sql_type =~ /\[\]$/ - @array = true - super(name, default_value, sql_type[0..sql_type.length - 3], null) - else - @array = false - super(name, default_value, sql_type, null) - end - - @default_function = default if has_default_function?(default_value, default) - end - - def number? - !array && super - end - - def text? - !array && super - end - - # :stopdoc: - class << self - include ConnectionAdapters::PostgreSQLColumn::Cast - include ConnectionAdapters::PostgreSQLColumn::ArrayParser - attr_accessor :money_precision - end - # :startdoc: - - # 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 - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ - $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end - end - - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end - - def accessor - @oid_type.accessor - end - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - end - - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super - end - end - - # 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 - when 'hstore' - :hstore - when 'ltree' - :ltree - # Network address types - when 'inet' - :inet - when 'cidr' - :cidr - when 'macaddr' - :macaddr - # 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(?:|\(\d+\))$/ - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :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' - :uuid - # JSON type - when 'json' - :json - # Small and big integer types - when /^(?:small|big)int$/ - :integer - when /(num|date|tstz|ts|int4|int8)range$/ - field_type.to_sym - # Pass through all types that are not specific to PostgreSQL. - else - super - end - end - end - # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. # # Options: @@ -287,142 +74,16 @@ module ActiveRecord # 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 ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - - module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], 'xml', options) - end - - def tsvector(*args) - options = args.extract_options! - column(args[0], 'tsvector', options) - end - - def int4range(name, options = {}) - column(name, 'int4range', options) - end - - def int8range(name, options = {}) - column(name, 'int8range', options) - end - - def tsrange(name, options = {}) - column(name, 'tsrange', options) - end - - def tstzrange(name, options = {}) - column(name, 'tstzrange', options) - end - - def numrange(name, options = {}) - column(name, 'numrange', options) - end - - def daterange(name, options = {}) - column(name, 'daterange', options) - end - - def hstore(name, options = {}) - column(name, 'hstore', options) - end - - def ltree(name, options = {}) - column(name, 'ltree', options) - end - - def inet(name, options = {}) - column(name, 'inet', options) - end - - def cidr(name, options = {}) - column(name, 'cidr', options) - end - - def macaddr(name, options = {}) - column(name, 'macaddr', options) - end - - def uuid(name, options = {}) - column(name, 'uuid', options) - end - - def json(name, options = {}) - column(name, 'json', options) - end - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options - end - - def column(name, type = nil, options = {}) - super - column = self[name] - column.array = options[:array] - - self - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new name, type - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - string: { name: "character varying", limit: 255 }, + string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, - timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, daterange: { name: "daterange" }, @@ -441,13 +102,20 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, - ltree: { name: "ltree" } + ltree: { name: "ltree" }, + citext: { name: "citext" }, + point: { name: "point" }, + bit: { name: "bit" }, + bit_varying: { name: "bit varying" }, + money: { name: "money" }, } - include Quoting - include ReferentialIntegrity - include SchemaStatements - include DatabaseStatements + OID = PostgreSQL::OID #:nodoc: + + include PostgreSQL::Quoting + include PostgreSQL::ReferentialIntegrity + include PostgreSQL::SchemaStatements + include PostgreSQL::DatabaseStatements include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -455,9 +123,13 @@ module ActiveRecord ADAPTER_NAME end + def schema_creation # :nodoc: + PostgreSQL::SchemaCreation.new self + end + # Adds `:array` option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) + def prepare_column_options(column, types) # :nodoc: spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array spec[:default] = "\"#{column.default_function}\"" if column.default_function @@ -487,6 +159,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -544,19 +220,15 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) + @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::PostgreSQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end @connection_parameters, @config = connection_parameters, config @@ -573,7 +245,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - @type_map = OID::TypeMap.new + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true @@ -592,10 +264,6 @@ module ActiveRecord false end - def active_threadsafe? - @connection.connect_poll != PG::PGRES_POLLING_FAILED - end - # Close then reopen the connection. def reconnect! super @@ -605,7 +273,12 @@ module ActiveRecord def reset! clear_cache! - super + reset_transaction + unless @connection.transaction_status == ::PG::PQTRANS_IDLE + @connection.query 'ROLLBACK' + end + @connection.query 'DISCARD ALL' + configure_connection end # Disconnects from the database if already connected. Otherwise, this @@ -637,10 +310,6 @@ module ActiveRecord self.client_min_messages = old end - def supports_insert_with_returning? - true - end - def supports_ddl_transactions? true end @@ -659,6 +328,10 @@ module ActiveRecord postgresql_version >= 90200 end + def supports_materialized_views? + postgresql_version >= 90300 + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -675,14 +348,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -699,25 +371,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - module Utils - extend self - - # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. - # +schema_name+ is nil if not specified in +name+. - # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) - # +name+ supports the range of schema/table references understood by PostgreSQL, for example: - # - # * <tt>table_name</tt> - # * <tt>"table.name"</tt> - # * <tt>schema_name.table_name</tt> - # * <tt>schema_name."table.name"</tt> - # * <tt>"schema.name"."table name"</tt> - def extract_schema_and_table(name) - table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse - [schema, table] - end - end - def use_insert_returning? @use_insert_returning end @@ -727,7 +380,12 @@ module ActiveRecord end def update_table_definition(table_name, base) #:nodoc: - Table.new(table_name, base) + PostgreSQL::Table.new(table_name, base) + end + + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) end protected @@ -756,63 +414,163 @@ module ActiveRecord private - def type_map - @type_map - end + def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: + if !type_map.key?(oid) + load_additional_types(type_map, [oid]) + end - def reload_type_map - type_map.clear - initialize_type_map(type_map) + type_map.fetch(oid, fmod, sql_type) { + warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end + } end - def add_oid(row, records_by_oid, type_map) - return type_map if type_map.key? row['type_elem'].to_i + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, 'int2', OID::Integer + m.alias_type 'int4', 'int2' + m.alias_type 'int8', 'int2' + m.alias_type 'oid', 'int2' + m.register_type 'float4', OID::Float.new + m.alias_type 'float8', 'float4' + m.register_type 'text', Type::Text.new + register_class_with_limit m, 'varchar', Type::String + m.alias_type 'char', 'varchar' + m.alias_type 'name', 'varchar' + m.alias_type 'bpchar', 'varchar' + m.register_type 'bool', Type::Boolean.new + register_class_with_limit m, 'bit', OID::Bit + register_class_with_limit m, 'varbit', OID::BitVarying + m.alias_type 'timestamptz', 'timestamp' + m.register_type 'date', OID::Date.new + m.register_type 'time', OID::Time.new + + m.register_type 'money', OID::Money.new + m.register_type 'bytea', OID::Bytea.new + m.register_type 'point', OID::Point.new + m.register_type 'hstore', OID::Hstore.new + m.register_type 'json', OID::Json.new + m.register_type 'jsonb', OID::Jsonb.new + m.register_type 'cidr', OID::Cidr.new + m.register_type 'inet', OID::Inet.new + m.register_type 'uuid', OID::Uuid.new + m.register_type 'xml', OID::Xml.new + m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) + m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) + m.register_type 'citext', OID::SpecializedString.new(:citext) + m.register_type 'ltree', OID::SpecializedString.new(:ltree) + + # FIXME: why are we keeping these types as strings? + m.alias_type 'interval', 'varchar' + m.alias_type 'path', 'varchar' + m.alias_type 'line', 'varchar' + m.alias_type 'polygon', 'varchar' + m.alias_type 'circle', 'varchar' + m.alias_type 'lseg', 'varchar' + m.alias_type 'box', 'varchar' + + m.register_type 'timestamp' do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::DateTime.new(precision: precision) + end - if OID.registered_type? row['typname'] - # this composite type is explicitly registered - vector = OID::NAMES[row['typname']] - else - # use the default for composite types - unless type_map.key? row['typelem'].to_i - add_oid records_by_oid[row['typelem']], records_by_oid, type_map + m.register_type 'numeric' do |_, fmod, sql_type| + precision = extract_precision(sql_type) + scale = extract_scale(sql_type) + + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if fmod && (fmod - 4 & 0xffff).zero? + # FIXME: Remove this class, and the second argument to + # lookups on PG + Type::DecimalWithoutScale.new(precision: precision) + else + OID::Decimal.new(precision: precision, scale: scale) end - - vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] end - type_map[row['oid'].to_i] = vector - type_map + load_additional_types(m) end - def initialize_type_map(type_map) - result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') - leaves, nodes = result.partition { |row| row['typelem'] == '0' } + def extract_limit(sql_type) # :nodoc: + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super + end + end - # populate the leaf nodes - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - type_map[row['oid'].to_i] = OID::NAMES[row['typname']] + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(oid, default) # :nodoc: + case default + # Quoted types + when /\A[\(B]?'(.*)'::/m + $1.gsub(/''/, "'") + # Boolean types + when 'true', 'false' + default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end + end - records_by_oid = result.group_by { |row| row['oid'] } + def extract_default_function(default_value, default) # :nodoc: + default if has_default_function?(default_value, default) + end - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + def has_default_function?(default_value, default) # :nodoc: + !default_value && (%r{\w+\(.*\)} === default) + end - # populate composite types - nodes.each do |row| - add_oid row, records_by_oid, type_map + def load_additional_types(type_map, oids = nil) # :nodoc: + if supports_ranges? + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype + FROM pg_type as t + LEFT JOIN pg_range as r ON oid = rngtypid + SQL + else + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + FROM pg_type as t + SQL end - # populate array types - arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row| - array = OID::Array.new type_map[row['typelem'].to_i] - type_map[row['oid'].to_i] = array + if oids + query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") end + + initializer = OID::TypeMapInitializer.new(type_map) + records = execute(query, 'SCHEMA') + initializer.run(records) end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: + def execute_and_clear(sql, name, binds) + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + ret = yield result + result.clear + ret + end + def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) @@ -870,11 +628,6 @@ module ActiveRecord @statements[sql_key] end - # The internal PostgreSQL identifier of the money data type. - MONEY_COLUMN_TYPE_OID = 790 #:nodoc: - # The internal PostgreSQL identifier of the BYTEA data type. - BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: - # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect @@ -883,14 +636,14 @@ module ActiveRecord # 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 # should know about this but can't detect it there, so deal with it here. - PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 + OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end @@ -965,7 +718,7 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) #:nodoc: + def column_definitions(table_name) # :nodoc: exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod @@ -977,23 +730,13 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) - match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) - - if match_data - rest = name[match_data[0].length, name.length] - rest = rest[1, rest.length] if rest.start_with? "." - [match_data[1], (rest.length > 0 ? rest : nil)] - end - end - - def extract_table_ref_from_insert_sql(sql) + def extract_table_ref_from_insert_sql(sql) # :nodoc: sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index e5c9f6f54a..a10ce330c7 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionAdapters class SchemaCache @@ -12,15 +11,15 @@ module ActiveRecord @columns_hash = {} @primary_keys = {} @tables = {} - prepare_default_proc end def primary_keys(table_name) - @primary_keys[table_name] + @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. def table_exists?(name) + prepare_tables if @tables.empty? return @tables[name] if @tables.key? name @tables[name] = connection.table_exists?(name) @@ -29,9 +28,9 @@ module ActiveRecord # Add internal cache for table with +table_name+. def add(table_name) if table_exists?(table_name) - @primary_keys[table_name] - @columns[table_name] - @columns_hash[table_name] + primary_keys(table_name) + columns(table_name) + columns_hash(table_name) end end @@ -40,14 +39,16 @@ module ActiveRecord end # Get the columns for a table - def columns(table) - @columns[table] + def columns(table_name) + @columns[table_name] ||= connection.columns(table_name) end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table) - @columns_hash[table] + def columns_hash(table_name) + @columns_hash[table_name] ||= Hash[columns(table_name).map { |col| + [col.name, col] + }] end # Clears out internal caches @@ -76,33 +77,18 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| - Hash[val] - } + [@version, @columns, @columns_hash, @primary_keys, @tables] end def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array - prepare_default_proc end private - def prepare_default_proc - @columns.default_proc = Proc.new do |h, table_name| - h[table_name] = connection.columns(table_name) - end - - @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns(table_name).map { |col| - [col.name, col] - }] + def prepare_tables + connection.tables.each { |table| @tables[table] = true } end - - @primary_keys.default_proc = Proc.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 3c5f7a981e..faf1cdc686 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. + # Allow database path relative to Rails.root, but only if the database + # path is not the special path that tells sqlite to build a database only + # in memory. if ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -30,24 +30,32 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] - ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end end module ConnectionAdapters #:nodoc: - class SQLite3Column < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value + class SQLite3Binary < Type::Binary # :nodoc: + def cast_value(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + + class SQLite3String < Type::String # :nodoc: + def type_cast_for_database(value) + if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT + value.encode(Encoding::UTF_8) + else + super end end end @@ -63,13 +71,12 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', - string: { name: "varchar", limit: 255 }, + string: { name: "varchar" }, text: { name: "text" }, integer: { name: "integer" }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, - timestamp: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, @@ -123,11 +130,7 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) + def initialize(connection, logger, connection_options, config) super(connection, logger) @active = nil @@ -135,11 +138,12 @@ module ActiveRecord self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config + @visitor = Arel::Visitors::SQLite.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::SQLite.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -225,10 +229,19 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" + def _quote(value) # :nodoc: + case value + when Type::Binary::Data + "x'#{value.hex}'" + else + super + end + end + + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f else super end @@ -256,24 +269,11 @@ module ActiveRecord end end - 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}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) end class ExplainPrettyPrinter @@ -299,9 +299,12 @@ module ActiveRecord # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close + begin + cols = stmt.columns + records = stmt.to_a + ensure + stmt.close + end stmt = records else cache = @statements[sql] ||= { @@ -382,7 +385,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] @@ -394,7 +397,9 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end - SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + sql_type = field['type'] + cast_type = lookup_cast_type(sql_type) + new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -495,14 +500,19 @@ module ActiveRecord end def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - rename_column_indexes(table_name, column_name, new_column_name) + column = column_for(table_name, column_name) + alter_table(table_name, rename: {column.name => new_column_name.to_s}) + rename_column_indexes(table_name, column.name, new_column_name) end protected + + def initialize_type_map(m) + super + m.register_type(/binary/i, SQLite3Binary.new) + register_class_with_limit m, %r(char)i, SQLite3String + end + def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 11f6a47158..31e7390bf7 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -18,14 +18,14 @@ module ActiveRecord # Example for SQLite database: # # ActiveRecord::Base.establish_connection( - # adapter: "sqlite", + # adapter: "sqlite3", # database: "path/to/dbfile" # ) # # Also accepts keys as strings (for parsing from YAML for example): # # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", + # "adapter" => "sqlite3", # "database" => "path/to/dbfile" # ) # @@ -58,9 +58,9 @@ module ActiveRecord end class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations, url = ENV['DATABASE_URL']) + def initialize(raw_configurations) @raw_config = raw_configurations.dup - @url = url + @env = DEFAULT_ENV.call.to_s end # Returns fully resolved connection hashes. @@ -71,38 +71,11 @@ module ActiveRecord private def config - if @url - raw_merged_into_default - else - @raw_config - end - end - - def raw_merged_into_default - default = default_url_hash - - @raw_config.each do |env, values| - default[env] = values || {} - default[env].merge!("url" => @url) { |h, v1, v2| v1 || v2 } if default[env].is_a?(Hash) - end - default - end - - # When the raw configuration is not present and ENV['DATABASE_URL'] - # is available we return a hash with the connection information in - # the connection URL. This hash responds to any string key with - # resolved connection information. - def default_url_hash - if @raw_config.blank? - Hash.new do |hash, key| - hash[key] = if key.is_a? String - ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(@url).to_hash - else - nil - end + @raw_config.dup.tap do |cfg| + if url = ENV['DATABASE_URL'] + cfg[@env] ||= {} + cfg[@env]["url"] ||= url end - else - {} end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index d9aaf8597f..d22806fbdf 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -16,7 +16,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -94,6 +93,7 @@ module ActiveRecord end class_attribute :default_connection_handler, instance_writer: false + class_attribute :find_by_statement_cache def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler @@ -107,6 +107,76 @@ module ActiveRecord end module ClassMethods + def allocate + define_attribute_methods + super + end + + def initialize_find_by_cache + self.find_by_statement_cache = {}.extend(Mutex_m) + end + + def inherited(child_class) + child_class.initialize_find_by_cache + super + end + + def find(*ids) + # We don't have cache keys for this stuff yet + return super unless ids.length == 1 + return super if block_given? || + primary_key.nil? || + default_scopes.any? || + columns_hash.include?(inheritance_column) || + ids.first.kind_of?(Array) + + id = ids.first + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ + "Please pass the id of the object by calling `.id`" + end + key = primary_key + + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + where(key => params.bind).limit(1) + } + } + record = s.execute([id], self, connection).first + unless record + raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + end + record + end + + def find_by(*args) + return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any? + + hash = args.first + + return super if hash.values.any? { |v| + v.nil? || Array === v || Hash === v + } + + key = hash.keys + + klass = self + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + wheres = key.each_with_object({}) { |param,o| + o[param] = params.bind + } + klass.where(wheres).limit(1) + } + } + begin + s.execute(hash.values, self, connection).first + rescue TypeError => e + raise ActiveRecord::StatementInvalid.new(e.message, e) + end + end + def initialize_generated_modules super @@ -183,16 +253,12 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.column_defaults.dup - defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } - - @attributes = self.class.initialize_attributes(defaults) - @column_types_override = nil - @column_types = self.class.column_types + @attributes = self.class.default_attributes.dup init_internals initialize_internals_callback + self.class.define_attribute_methods # +options+ argument is only needed to make protected_attributes gem easier to hook. # Remove it when we drop support to this gem. init_attributes(attributes, options) if attributes @@ -212,13 +278,13 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) - @column_types_override = coder['column_types'] - @column_types = self.class.column_types + @attributes = coder['attributes'] init_internals - @new_record = false + @new_record = coder['new_record'] + + self.class.define_attribute_methods run_callbacks :find run_callbacks :initialize @@ -254,19 +320,16 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes, :serialized => false) - - @attributes = cloned_attributes - @attributes[self.class.primary_key] = nil + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) run_callbacks(:initialize) unless _initialize_callbacks.empty? @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} @new_record = true + @destroyed = false super end @@ -284,7 +347,10 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes_for_coder + # FIXME: Remove this when we better serialize attributes + coder['raw_attributes'] = attributes_before_type_cast + coder['attributes'] = @attributes + coder['new_record'] = new_record? end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -299,7 +365,7 @@ module ActiveRecord def ==(comparison_object) super || comparison_object.instance_of?(self.class) && - id && + !id.nil? && comparison_object.id == id end alias :eql? :== @@ -307,7 +373,11 @@ module ActiveRecord # 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 + if id + id.hash + else + super + end end # Clone and freeze the attributes hash such that associations are still @@ -363,6 +433,29 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access @@ -426,12 +519,10 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) + @attributes.ensure_initialized(self.class.primary_key) @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -451,5 +542,11 @@ module ActiveRecord def init_attributes(attributes, options) assign_attributes(attributes) end + + def thaw + if frozen? + @attributes = @attributes.dup + end + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index dcbdf75627..f0b6afc4b4 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more association counters to reset + # * +counters+ - One or more association counters to reset. Association name or counter name can be given. # # ==== Examples # @@ -19,9 +19,14 @@ module ActiveRecord # Post.reset_counters(1, :comments) def reset_counters(id, *counters) object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) - raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association + counters.each do |counter_association| + has_many_association = _reflect_on_association(counter_association.to_sym) + unless has_many_association + has_many = reflect_on_all_associations(:has_many) + has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } + counter_association = has_many_association.plural_name if has_many_association + end + raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection @@ -29,12 +34,11 @@ module ActiveRecord 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.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count + arel_table[counter_name] => object.send(counter_association).count(:all) }, primary_key) connection.update stmt end @@ -118,5 +122,54 @@ module ActiveRecord update_counters(id, counter_name => -1) end end + + protected + + def actually_destroyed? + @_actually_destroyed + end + + def clear_destroy_state + @_actually_destroyed = nil + end + + private + + def _create_record(*) + id = super + + each_counter_cached_associations do |association| + if send(association.reflection.name) + association.increment_counters + @_after_create_counter_called = true + end + end + + id + end + + def destroy_row + affected_rows = super + + if affected_rows > 0 + each_counter_cached_associations do |association| + foreign_key = association.reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + if send(association.reflection.name) + association.decrement_counters + end + end + end + end + + affected_rows + end + + def each_counter_cached_associations + _reflections.each do |name, reflection| + yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + end + end + end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 4aa323fb00..f0ee433d0b 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/deep_dup' + module ActiveRecord # Declare an enum attribute where the values map to integers in the database, # but can be queried by name. Example: @@ -25,8 +27,10 @@ module ActiveRecord # conversation.status # => nil # # Scopes based on the allowed values of the enum field will be provided - # as well. With the above example, it will create an +active+ and +archived+ - # scope. + # as well. With the above example: + # + # Conversation.active + # Conversation.archived # # You can set the default value from the database declaration, like: # @@ -65,10 +69,14 @@ module ActiveRecord # # Where conditions on an enum attribute must use the ordinal value of an enum. module Enum - DEFINED_ENUMS = {} # :nodoc: + def self.extended(base) # :nodoc: + base.class_attribute(:defined_enums) + base.defined_enums = {} + end - def enum_mapping_for(attr_name) # :nodoc: - DEFINED_ENUMS[attr_name.to_s] + def inherited(base) # :nodoc: + base.defined_enums = defined_enums.deep_dup + super end def enum(definitions) @@ -122,9 +130,8 @@ module ActiveRecord klass.send(:detect_enum_conflict!, name, value, true) klass.scope value, -> { klass.where name => i } end - - DEFINED_ENUMS[name.to_s] = enum_values end + defined_enums[name.to_s] = enum_values end end @@ -133,17 +140,14 @@ module ActiveRecord @_enum_methods_module ||= begin mod = Module.new do private - def save_changed_attribute(attr_name, value) - if (mapping = self.class.enum_mapping_for(attr_name)) + def save_changed_attribute(attr_name, old) + if (mapping = self.class.defined_enums[attr_name.to_s]) + value = read_attribute(attr_name) if attribute_changed?(attr_name) - old = changed_attributes[attr_name] - if mapping[old] == value changed_attributes.delete(attr_name) end else - old = clone_attribute_value(:read_attribute, attr_name) - if old != value changed_attributes[attr_name] = mapping.key old end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 7f6228131f..52c70977ef 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,17 +30,18 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> - # misses adapter field). + # Raised when adapter not specified on connection (or configuration file + # +config/database.yml+ misses adapter field). class AdapterNotSpecified < ActiveRecordError end - # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically. + # Raised when Active Record cannot find database adapter specified in + # +config/database.yml+ or programmatically. class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> - # is given a nil object). + # Raised when connection to the database could not been established (for + # example when +connection=+ is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -82,35 +83,26 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, - # when using +find+ method) - # does not match number of expected variables. + # Raised when number of bind variables in statement given to +:condition+ key + # (for example, when using +find+ method) does not match number of expected + # values supplied. # - # For example, in + # For example, when there are two placeholders with only one value supplied: # # Location.where("lat = ? AND lng = ?", 53.7362) - # - # two placeholders are given but only one variable to fill them. class PreparedStatementInvalid < ActiveRecordError end - # Raised when a given database does not exist - class NoDatabaseError < ActiveRecordError - def initialize(message) - super extend_message(message) - end - - # can be over written to add additional error information. - def extend_message(message) - message - end + # Raised when a given database does not exist. + class NoDatabaseError < StatementInvalid end # Raised on attempt to save stale record. Record is stale when it's being saved in another query after # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. # - # Read more about optimistic locking in ActiveRecord::Locking module RDoc. + # Read more about optimistic locking in ActiveRecord::Locking module + # documentation. class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action @@ -122,8 +114,9 @@ module ActiveRecord end - # Raised when association is being configured improperly or - # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations. + # Raised when association is being configured improperly or user tries to use + # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # associations. class ConfigurationError < ActiveRecordError end @@ -161,7 +154,8 @@ module ActiveRecord class Rollback < ActiveRecordError end - # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods). + # Raised when attribute has a name reserved by Active Record (when attribute + # has name of one of Active Record instance methods). class DangerousAttributeError < ActiveRecordError end @@ -179,7 +173,7 @@ module ActiveRecord end # Raised when an error occurred while doing a mass assignment to an attribute through the - # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the + # +attributes=+ method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute @@ -225,6 +219,13 @@ module ActiveRecord class ImmutableRelation < ActiveRecordError end + # TransactionIsolationError will be raised under the following conditions: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index e65dab07ba..727a9befc1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -27,7 +27,7 @@ module ActiveRecord end.join("\n") end.join("\n") - # Overriding inspect to be more human readable, specially in the console. + # Overriding inspect to be more human readable, especially in the console. def str.inspect self end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 297792aeec..4306b36ae1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,6 +2,7 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' +require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -14,9 +15,10 @@ module ActiveRecord # They are stored in YAML files, one file per model, which are placed in the directory # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). - # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: - # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks - # like this: + # The fixture file ends with the +.yml+ file extension, for example: + # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). + # + # The format of a fixture file looks like this: # # rubyonrails: # id: 1 @@ -32,7 +34,7 @@ module ActiveRecord # is followed by an indented list of key/value pairs in the "key: value" format. Records are # separated by a blank line for your viewing pleasure. # - # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type. + # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type. # See http://yaml.org/type/omap.html # for the specification. You will need ordered fixtures when you have foreign key constraints # on keys in the same table. This is commonly needed for tree structures. Example: @@ -60,8 +62,8 @@ module ActiveRecord # end # end # - # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database, - # so this test will succeed. + # By default, +test_helper.rb+ will load all of your fixtures into your test + # database, so this test will succeed. # # The testing environment will automatically load the all fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. @@ -361,6 +363,7 @@ module ActiveRecord # geeksomnia: # name: Geeksomnia's Account # subdomain: $LABEL + # email: $LABEL@email.com # # Also, sometimes (like when porting older join table fixtures) you'll need # to be able to get a hold of the identifier for a given label. ERB @@ -372,8 +375,9 @@ module ActiveRecord # # == Support for YAML defaults # - # You probably already know how to use YAML to set and reuse defaults in - # your <tt>database.yml</tt> file. You can use the same technique in your fixtures: + # You can set and reuse defaults in your fixtures YAML file. + # This is the same technique used in the +database.yml+ file to specify + # defaults: # # DEFAULTS: &DEFAULTS # created_on: <%= 3.weeks.ago.to_s(:db) %> @@ -389,7 +393,8 @@ module ActiveRecord # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet #-- - # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and + # possibly in a folder with the same name. #++ MAX_ID = 2 ** 30 - 1 @@ -459,13 +464,7 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |k,klass| - unless klass.is_a? Class - klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") - end - !insert_class(@class_names, k, klass) - } + @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } end def [](fs_name) @@ -549,9 +548,13 @@ module ActiveRecord end # Returns a consistent, platform-independent identifier for +label+. - # Identifiers are positive integers less than 2^32. - def self.identify(label) - Zlib.crc32(label.to_s) % MAX_ID + # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. + def self.identify(label, column_type = :integer) + if column_type == :uuid + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) + else + Zlib.crc32(label.to_s) % MAX_ID + end end # Superclass for the evaluation contexts used by ERB fixtures. @@ -567,10 +570,6 @@ module ActiveRecord @config = config @model_class = nil - if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") - end - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @@ -627,12 +626,12 @@ module ActiveRecord # interpolate the fixture label row.each do |key, value| - row[key] = label if "$LABEL" == value + row[key] = value.gsub("$LABEL", label) if value.is_a?(String) end # generate a primary key if necessary if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::FixtureSet.identify(label) + row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) end # If STI is used, find the correct subclass for association reflection @@ -643,19 +642,20 @@ module ActiveRecord model_class end - reflection_class.reflect_on_all_associations.each do |association| + reflection_class._reflections.values.each do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") # support polymorphic belongs_to as "label (Type)" row[association.foreign_type] = $1 end - row[fk_name] = ActiveRecord::FixtureSet.identify(value) + fk_type = association.active_record.columns_hash[association.foreign_key].type + row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many if association.options[:through] @@ -682,6 +682,10 @@ module ActiveRecord def name @association.name end + + def primary_key_type + @association.klass.column_types[@association.klass.primary_key].type + end end class HasManyThroughProxy < ReflectionProxy # :nodoc: @@ -699,17 +703,22 @@ module ActiveRecord @primary_key_name ||= model_class && model_class.primary_key end + def primary_key_type + @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type + end + def add_join_records(rows, row, association) # This is the case when the join table has no fixtures file if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - lhs_key = association.lhs_key - rhs_key = association.rhs_key + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) rows[table_name].concat targets.map { |target| { lhs_key => row[primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target) } + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } } end end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb new file mode 100644 index 0000000000..4a7aace460 --- /dev/null +++ b/activerecord/lib/active_record/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveRecord + # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..251d682a02 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern @@ -120,14 +151,8 @@ module ActiveRecord candidates << type_name candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - # We don't want to swallow NoMethodError < NameError errors - rescue NoMethodError - raise - rescue NameError - end + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 6f54729b3c..52eeb8ae1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,7 +66,7 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def update_record(attribute_names = @attributes.keys) #:nodoc: + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -141,7 +141,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @column_defaults = nil + clear_caches_calculated_from_columns @locking_column = value.to_s end @@ -151,12 +151,6 @@ module ActiveRecord @locking_column end - # Quote the column name used for optimistic locking. - def quoted_locking_column - ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." - connection.quote_column_name(locking_column) - end - # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN @@ -169,18 +163,42 @@ module ActiveRecord super end - def column_defaults - @column_defaults ||= begin - defaults = super - - if defaults.key?(locking_column) && lock_optimistically - defaults[locking_column] ||= 0 + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) end - - defaults end + super end end end + + class LockingType < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + # `nil` *should* be changed to 0 + super.to_i + end + + def changed?(old_value, *) + # Ensure we save if the default was `nil` + super || old_value == 0 + end + + def init_with(coder) + __setobj__(coder['subtype']) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 654ef21b07..eb64d197f0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -25,7 +25,7 @@ module ActiveRecord if column.binary? # This specifically deals with the PG adapter that casts bytea columns into a Hash. value = value[:value] if value.is_a?(Hash) - value = "<#{value.bytesize} bytes of binary data>" + value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" end [column.name, value] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b6b02322d7..a6847e28c2 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -195,7 +195,7 @@ module ActiveRecord # == Database support # # Migrations are currently supported in MySQL, PostgreSQL, SQLite, - # SQL Server, Sybase, and Oracle (all supported databases except DB2). + # SQL Server, and Oracle (all supported databases except DB2). # # == More examples # @@ -372,13 +372,21 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -391,7 +399,7 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? - ActiveRecord::Tasks::DatabaseTasks.load_schema + ActiveRecord::Tasks::DatabaseTasks.load_schema_current check_pending! end end @@ -640,9 +648,11 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert - unless arguments.empty? || method == :execute + unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) @@ -711,7 +721,7 @@ module ActiveRecord if ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max else - "%.3d" % number + SchemaMigration.normalize_migration_number(number) end end @@ -804,22 +814,22 @@ module ActiveRecord migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:up, migrations, target_version).migrate + new(:up, migrations, target_version).migrate end 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 + new(:down, migrations, target_version).migrate end def run(direction, migrations_paths, target_version) - self.new(direction, migrations(migrations_paths), target_version).run + new(direction, migrations(migrations_paths), target_version).run end def open(migrations_paths) - self.new(:up, migrations(migrations_paths), nil) + new(:up, migrations(migrations_paths), nil) end def schema_migrations_table_name @@ -851,19 +861,6 @@ module ActiveRecord migrations(migrations_paths).last || NullMigration.new end - def proper_table_name(name, options = {}) - ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" - options = { - table_name_prefix: ActiveRecord::Base.table_name_prefix, - table_name_suffix: ActiveRecord::Base.table_name_suffix - }.merge(options) - if name.respond_to? :table_name - name.table_name - else - "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" - end - end - def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string @@ -895,7 +892,7 @@ module ActiveRecord private def move(direction, migrations_paths, steps) - migrator = self.new(direction, migrations(migrations_paths)) + migrator = new(direction, migrations(migrations_paths)) start_index = migrator.migrations.index(migrator.current_migration) if start_index diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..36256415df 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -85,7 +87,7 @@ module ActiveRecord alias :add_belongs_to :add_reference alias :remove_belongs_to :remove_reference - def change_table(table_name, options = {}) + def change_table(table_name, options = {}) # :nodoc: yield delegate.update_table_definition(table_name, self) end @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index ebf64cbcdc..05569fadbd 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -8,7 +8,7 @@ module ActiveRecord end def join_table_name(table_1, table_2) - [table_1.to_s, table_2.to_s].sort.join("_").to_sym + ModelSchema.derive_join_table_name(table_1, table_2).to_sym end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index dc5ff02882..850220babd 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -29,6 +29,10 @@ module ActiveRecord # :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. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. class_attribute :table_name_suffix, instance_writer: false self.table_name_suffix = "" @@ -47,6 +51,19 @@ module ActiveRecord self.pluralize_table_names = true self.inheritance_column = 'type' + + delegate :type_for_attribute, to: :class + end + + # Derives the join table name for +first_table+ and +second_table+. The + # table names appear in alphabetical order. A common prefix is removed + # (useful for namespaced models like Music::Artist and Music::Record): + # + # artists, records => artists_records + # records, artists => artists_records + # music_artists, music_records => music_artists_records + def self.derive_join_table_name(first_table, second_table) # :nodoc: + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") end module ClassMethods @@ -153,6 +170,10 @@ module ActiveRecord (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end + def full_table_name_suffix #:nodoc: + (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -190,7 +211,7 @@ module ActiveRecord # 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, + # If a sequence name is not explicitly set when using Oracle, # it will default to the commonly used pattern of: #{table_name}_seq # # If a sequence name is not explicitly set when using PostgreSQL, it @@ -209,50 +230,29 @@ module ActiveRecord 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] }] + def attributes_builder # :nodoc: + @attributes_builder ||= AttributeSet::Builder.new(column_types) end def column_types # :nodoc: - @column_types ||= decorate_columns(columns_hash.dup) - end - - def decorate_columns(columns_hash) # :nodoc: - return if columns_hash.empty? - - @serialized_column_names ||= self.columns_hash.keys.find_all do |name| - serialized_attributes.key?(name) - end - - @serialized_column_names.each do |name| - columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name]) - end - - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| - create_time_zone_conversion_attribute?(name, col) - end.map!(&:first) - - @time_zone_column_names.each do |name| - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) + @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| + h.default = Type::Value.new end + end - columns_hash + def type_for_attribute(attr_name) # :nodoc: + column_types[attr_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 - @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + default_attributes.to_hash + end + + def default_attributes # :nodoc: + @default_attributes ||= attributes_builder.build_from_database( + columns_hash.transform_values(&:default)) end # Returns an array of column names as strings. @@ -263,7 +263,7 @@ module ActiveRecord # 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 } + @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end # Resets all the cached information about columns, which will cause them @@ -298,27 +298,17 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil - @column_defaults = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil + @default_attributes = nil @dynamic_methods_hash = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil - @serialized_column_names = nil @time_zone_column_names = nil @cached_time_zone = nil end - # This is a hook for use by modules that need to do extra stuff to - # attributes when they are initialized. (e.g. attribute - # serialization) - def initialize_attributes(attributes, options = {}) #:nodoc: - attributes - end - private # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -337,7 +327,8 @@ module ActiveRecord contained = contained.singularize if parent.pluralize_table_names contained += '_' end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. base.table_name diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 9d92e747d4..8a2a06f2ca 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -305,7 +305,7 @@ module ActiveRecord options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| - if reflection = reflect_on_association(association_name) + if reflection = _reflect_on_association(association_name) reflection.autosave = true add_autosave_association_callbacks(reflection) @@ -485,10 +485,10 @@ module ActiveRecord end # Takes in a limit and checks if the attributes_collection has too many - # records. The method will take limits in the form of symbols, procs, and - # number-like objects (anything that can be compared with an integer). + # records. It accepts limit in the form of symbol, proc, or + # number-like object (anything that can be compared with an integer). # - # Will raise an TooManyRecords error if the attributes_collection is + # Raises TooManyRecords error if the attributes_collection is # larger than the limit. def check_record_limit!(limit, attributes_collection) if limit @@ -516,10 +516,10 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) + Type::Boolean.new.type_cast_from_user(hash['_destroy']) end - # Determines if a new record should be build by checking for + # Determines if a new record should be rejected by checking # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) @@ -542,7 +542,7 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 5b255c3fe5..807c301596 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -23,7 +23,7 @@ module ActiveRecord end def size - 0 + calculate :size, nil end def empty? @@ -43,18 +43,32 @@ module ActiveRecord end def count(*) - 0 + calculate :count, nil end def sum(*) - 0 + calculate :sum, nil + end + + def average(*) + calculate :average, nil + end + + def minimum(*) + calculate :minimum, nil + end + + def maximum(*) + calculate :maximum, nil end def calculate(operation, _column_name, _options = {}) # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. - if operation == :count - 0 + if [:count, :sum, :size].include? operation + group_values.any? ? Hash.new : 0 + elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? + Hash.new else nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b1b35ed940..51b1931ed5 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -36,8 +36,25 @@ module ActiveRecord end end + # Creates an object (or multiple objects) and saves it to the database, + # if validations pass. Raises a RecordInvalid error if validations fail, + # unlike Base#create. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. + # These describe which attributes to be created on the object, or + # multiple objects when given an Array of Hashes. + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + object = new(attributes, &block) + object.save! + object + end + end + # Given an attributes hash, +instantiate+ returns a new instance of - # the appropriate class. + # the appropriate class. Accepts only keys as strings. # # For example, +Post.all+ may return Comments, Messages, and Emails # by storing the record's subclass in a +type+ attribute. By calling @@ -46,10 +63,10 @@ module ActiveRecord # # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see # how this "single-table" inheritance mapping is implemented. - def instantiate(record, column_types = {}) - klass = discriminate_class_for_record(record) - column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with('attributes' => record, 'column_types' => column_types) + def instantiate(attributes, column_types = {}) + klass = discriminate_class_for_record(attributes) + attributes = klass.attributes_builder.build_from_database(attributes, column_types) + klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end private @@ -64,7 +81,7 @@ module ActiveRecord 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. + # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? sync_with_transaction_state @new_record @@ -180,7 +197,6 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@attributes_cache", @attributes_cache) became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -214,6 +230,8 @@ module ActiveRecord # # This method raises an +ActiveRecord::ActiveRecordError+ if the # attribute is marked as readonly. + # + # See also +update_column+. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) @@ -269,7 +287,8 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "cannot update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -394,24 +413,24 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types - @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes_cache = {} + @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end # Saves the record with the updated_at/on attributes set to the current time. - # Please note that no validation is performed and only the +after_touch+ - # callback is executed. - # If an attribute name is passed, that attribute is updated along with - # updated_at/on attributes. + # Please note that no validation is performed and only the +after_touch+, + # +after_commit+ and +after_rollback+ callbacks are executed. + # + # If attribute names are passed, they are updated along with updated_at/on + # attributes. # - # product.touch # updates updated_at/on - # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on + # product.touch # updates updated_at/on + # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on + # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes # - # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object. + # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on + # associated object. # # class Brake < ActiveRecord::Base # belongs_to :car, touch: true @@ -430,11 +449,11 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(name = nil) + def touch(*names) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? attributes = timestamp_attributes_for_update_in_model - attributes << name if name + attributes.concat(names) unless attributes.empty? current_time = current_time_from_proper_timezone @@ -450,6 +469,8 @@ module ActiveRecord changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 + else + true end end @@ -477,24 +498,24 @@ module ActiveRecord def create_or_update raise ReadOnlyRecord if readonly? - result = new_record? ? create_record : update_record + result = new_record? ? _create_record : _update_record result != false end # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def update_record(attribute_names = @attributes.keys) + def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? 0 else - self.class.unscoped.update_record attributes_values, id, id_was + self.class.unscoped._update_record attributes_values, id, id_was end end # Creates a record with values matching those of the instance attributes # and returns its id. - def create_record(attribute_names = @attributes.keys) + def _create_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..dcb2bd3d84 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Query Cache class QueryCache @@ -29,9 +28,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index ef138c6f80..a9ddd9141f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -37,14 +37,7 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} - - if result_set.respond_to? :column_types - column_types = result_set.column_types - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - + column_types = result_set.column_types.except(*columns_hash.keys) result_set.map { |record| instantiate(record, column_types) } end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 11b564f8f9..a4ceacbf44 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -116,17 +116,22 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do + self.configurations = Rails.application.config.database_configuration - class ActiveRecord::NoDatabaseError - remove_possible_method :extend_message - def extend_message(message) - message << "Run `$ bin/rake db:create db:migrate` to create your database" - message - end - end + begin + establish_connection + rescue ActiveRecord::NoDatabaseError + warn <<-end_warning +Oops - You have a database configured, but it doesn't exist yet! - self.configurations = Rails.application.config.database_configuration - establish_connection +Here's how to get started: + + 1. Configure your database in config/database.yml. + 2. Run `bin/rake db:create` to create the database. + 3. Run `bin/rake db:setup` to load your database schema. +end_warning + raise + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1d5c80bc01..458862a538 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -28,12 +28,20 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_current end + namespace :purge do + task :all => :load_config do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.purge_current + end + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." 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) do |migration| - ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) - end + ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end @@ -82,22 +90,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) - puts 'Schema migrations table does not exist yet.' - next # means "return" for rake task + unless ActiveRecord::SchemaMigration.table_exists? + abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| "%.3d" % version } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -105,8 +112,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -178,17 +185,21 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -200,12 +211,7 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) @@ -234,7 +240,7 @@ db_namespace = namespace :db do desc 'Load a schema.rb file into the database' task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end task :load_if_ruby => ['db:create', :environment] do @@ -268,7 +274,8 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - if ActiveRecord::Base.connection.supports_migrations? + if ActiveRecord::Base.connection.supports_migrations? && + ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information f.print "\n" @@ -277,9 +284,9 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - # desc "Recreate the databases from the structure.sql file" + desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) end task :load_if_sql => ['db:create', :environment] do @@ -310,9 +317,8 @@ db_namespace = namespace :db do task :load_schema => %w(db:test:deprecated db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -322,12 +328,7 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => %w(db:test:deprecated db:test:purge) do - begin - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) - db_namespace["structure:load"].invoke - ensure - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) - end + ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" @@ -366,7 +367,7 @@ namespace :railties do task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } railties = {} - Rails.application.railties.each do |railtie| + Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index b3ddfd63d4..85bbac43e4 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index bce7766501..1547c8e3f4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,35 +1,44 @@ +require 'thread' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern included do - class_attribute :reflections + class_attribute :_reflections class_attribute :aggregate_reflections - self.reflections = {} + self._reflections = {} self.aggregate_reflections = {} end def self.create(macro, name, scope, options, ar) - case macro - when :has_many, :belongs_to, :has_one - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - klass.new(macro, name, scope, options, ar) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) - ar.reflections = ar.reflections.merge(name => reflection) + ar._reflections = ar._reflections.merge(name.to_s => reflection) end def self.add_aggregate_reflection(ar, name, reflection) - ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end - # \Reflection enables to interrogate Active Record classes and objects + # \Reflection enables interrogating Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type @@ -48,7 +57,25 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - aggregate_reflections[aggregation] + aggregate_reflections[aggregation.to_s] + end + + # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # + # Account.reflections # => {balance: AggregateReflection} + # + # @api public + def reflections + ref = {} + _reflections.each do |name, reflection| + parent_name, parent_reflection = reflection.parent_reflection + if parent_name + ref[parent_name] = parent_reflection + else + ref[name] = reflection + end + end + ref end # Returns an array of AssociationReflection objects for all the @@ -61,6 +88,7 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # + # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections @@ -71,36 +99,82 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # + # @api public def reflect_on_association(association) - reflections[association] + reflections[association.to_s] + end + + # @api private + def _reflect_on_association(association) #:nodoc: + _reflections[association.to_s] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. + # + # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + JoinKeys.new(foreign_key, active_record_primary_key) + end + + def source_macro + ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ + "will be removed without replacement.") + macro + end + end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection - # AggregateReflection # AssociationReflection - # ThroughReflection - class MacroReflection + # AggregateReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # ThroughReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name - # Returns the macro type. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> - # <tt>has_many :clients</tt> returns <tt>:has_many</tt> - attr_reader :macro - attr_reader :scope # Returns the hash of options used for the macro. @@ -113,8 +187,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -127,6 +200,10 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave + _, parent_reflection = self.parent_reflection + if parent_reflection + parent_reflection.autosave = autosave + end end # Returns the class for the macro. @@ -134,15 +211,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -151,7 +224,7 @@ module ActiveRecord super || other_aggregation.kind_of?(self.class) && name == other_aggregation.name && - other_aggregation.options && + !other_aggregation.options.nil? && active_record == other_aggregation.active_record end @@ -187,38 +260,40 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type + attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = :has_many == macro @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) + @association_scope_cache = {} + @scope_lock = Mutex.new end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) + def association_scope_cache(conn, owner) + key = conn.prepared_statements + if polymorphic? + key = [key, owner.read_attribute(@foreign_type)] + end + @association_scope_cache[key] ||= @scope_lock.synchronize { + @association_scope_cache[key] ||= yield + } end def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -227,10 +302,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_column - klass.columns_hash[klass.primary_key] - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -257,13 +328,31 @@ module ActiveRecord end def check_validity_of_inverse! - unless options[:polymorphic] + unless polymorphic? if has_inverse? && inverse_of.nil? raise InverseOfAssociationNotFoundError.new(self) end end end + def check_preloadable! + return unless scope + + if scope.arity > 0 + ActiveSupport::Deprecation.warn <<-WARNING +The association scope '#{name}' is instance dependent (the scope block takes an argument). +Preloading happens before the individual instances are created. This means that there is no instance +being passed to the association scope. This will most likely result in broken or incorrect behavior. +Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future. + WARNING + end + end + alias :check_eager_loadable! :check_preloadable! + + def join_id_for(owner) # :nodoc: + owner[active_record_primary_key] + end + def through_reflection nil end @@ -288,8 +377,6 @@ module ActiveRecord scope ? [[scope]] : [[]] end - alias :source_macro :macro - def has_inverse? inverse_name end @@ -297,12 +384,12 @@ module ActiveRecord def inverse_of return unless inverse_name - @inverse_of ||= klass.reflect_on_association inverse_name + @inverse_of ||= klass._reflect_on_association inverse_name end def polymorphic_inverse_of(associated_class) if has_inverse? - if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) + if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]) inverse_relationship else raise InverseOfAssociationNotFoundError.new(self, associated_class) @@ -310,11 +397,16 @@ module ActiveRecord end end + # Returns the macro type. + # + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + def macro; raise NotImplementedError; end + # Returns whether or not this association reflection is for a collection # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -327,18 +419,19 @@ module ActiveRecord # * you use autosave; <tt>autosave: true</tt> # * the association is a +has_many+ association def validate? - !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) + !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?) end # Returns +true+ if +self+ is a +belongs_to+ reflection. - def belongs_to? - macro == :belongs_to - end + def belongs_to?; false; end + + # Returns +true+ if +self+ is a +has_one+ reflection. + def has_one?; false; end def association_class case macro when :belongs_to - if options[:polymorphic] + if polymorphic? Associations::BelongsToPolymorphicAssociation else Associations::BelongsToAssociation @@ -359,7 +452,7 @@ module ActiveRecord end def polymorphic? - options.key? :polymorphic + options[:polymorphic] end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] @@ -376,7 +469,7 @@ module ActiveRecord def calculate_constructable(macro, options) case macro when :belongs_to - !options[:polymorphic] + !polymorphic? when :has_one !options[:through] else @@ -400,10 +493,10 @@ module ActiveRecord # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym begin - reflection = klass.reflect_on_association(inverse_name) + reflection = klass._reflect_on_association(inverse_name) rescue NameError # Give up: we couldn't compute the klass type so we won't be able # to find any associations either. @@ -449,9 +542,9 @@ module ActiveRecord end def derive_class_name - class_name = name.to_s.camelize + class_name = name.to_s class_name = class_name.singularize if collection? - class_name + class_name.camelize end def derive_foreign_key @@ -465,7 +558,7 @@ module ActiveRecord end def derive_join_table - [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end def primary_key(klass) @@ -473,15 +566,72 @@ module ActiveRecord end end + class HasManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_many; end + + def collection?; true; end + end + + class HasOneReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_one; end + + def has_one?; true; end + end + + class BelongsToReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :belongs_to; end + + def belongs_to?; true; end + + def join_keys(assoc_klass) + key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key + JoinKeys.new(key, foreign_key) + end + + def join_id_for(owner) # :nodoc: + owner[foreign_key] + end + end + + class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super + end + + def macro; :has_and_belongs_to_many; end + + def collection? + true + end + end + # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - def initialize(macro, name, scope, options, active_record) - super - @source_reflection_name = options[:source] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -499,10 +649,10 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.source_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection - through_reflection.klass.reflect_on_association(source_reflection_name) + through_reflection.klass._reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -515,10 +665,10 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.through_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> + # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - active_record.reflect_on_association(options[:through]) + active_record._reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -535,8 +685,8 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.chain - # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, - # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, + # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # def chain @chain ||= begin @@ -586,8 +736,14 @@ module ActiveRecord end end + def join_keys(assoc_klass) + source_reflection.join_keys(assoc_klass) + end + # The macro used by the source association def source_macro + ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ + "will be removed without replacement.") source_reflection.source_macro end @@ -617,7 +773,7 @@ module ActiveRecord # # => [:tag, :tags] # def source_reflection_names - (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq + options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq end def source_reflection_name # :nodoc: @@ -625,7 +781,7 @@ module ActiveRecord names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq names = names.find_all { |n| - through_reflection.klass.reflect_on_association(n) + through_reflection.klass._reflect_on_association(n) } if names.length > 1 @@ -653,12 +809,16 @@ directive on your declaration like: through_reflection.options end + def join_id_for(owner) # :nodoc: + source_reflection.join_id_for(owner) + end + def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end - if through_reflection.options[:polymorphic] + if through_reflection.polymorphic? raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) end @@ -666,15 +826,15 @@ directive on your declaration like: raise HasManyThroughSourceAssociationNotFoundError.new(self) end - if options[:source_type] && source_reflection.options[:polymorphic].nil? + if options[:source_type] && !source_reflection.polymorphic? raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) end - if source_reflection.options[:polymorphic] && options[:source_type].nil? + if source_reflection.polymorphic? && options[:source_type].nil? raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - if macro == :has_one && through_reflection.collection? + if has_one? && through_reflection.collection? raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end @@ -683,15 +843,25 @@ directive on your declaration like: protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index fb213dc6f7..ad54d84665 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +require 'arel/collectors/bind' module ActiveRecord # = Active Record Relation @@ -11,6 +12,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :distinct, :create_with, :uniq] + INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -70,9 +72,16 @@ module ActiveRecord binds) end - def update_record(values, id, id_was) # :nodoc: + def _update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values - um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + + scope = @klass.unscoped + + if @klass.finder_needs_type_condition? + scope.unscope!(where: @klass.inheritance_column) + end + + um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, @@ -223,6 +232,7 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain + #TODO: Fix for binds. exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -232,13 +242,18 @@ module ActiveRecord @records end + # Serializes the relation objects Array. + def encode_with(coder) + coder.represent_seq(nil, to_a) + end + def as_json(options = nil) #:nodoc: to_a.as_json(options) end # Returns size of the records. def size - loaded? ? @records.length : count + loaded? ? @records.length : count(:all) end # Returns true if there are no records. @@ -248,8 +263,7 @@ module ActiveRecord if limit_value == 0 true else - # FIXME: This count is not compatible with #select('authors.*') or other select narrows - c = count + c = count(:all) c.respond_to?(:zero?) ? c.zero? : c.empty? end end @@ -324,7 +338,8 @@ module ActiveRecord stmt.wheres = arel.constraints end - @klass.connection.update stmt, 'SQL', bind_values + bvs = bind_values + arel.bind_values + @klass.connection.update stmt, 'SQL', bvs end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -428,12 +443,21 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. # - # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: # # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit def delete_all(conditions = nil) - raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| + if MULTI_VALUE_METHODS.include?(method) + send("#{method}_values").any? + else + send("#{method}_value") + end + } + if invalid_methods.any? + raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") + end if conditions where(conditions).delete_all @@ -517,11 +541,11 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - ast = relation.arel.ast - binds = relation.bind_values.dup - visitor.accept(ast) do - connection.quote(*binds.shift.reverse) - end + arel = relation.arel + binds = (arel.bind_values + relation.bind_values).dup + binds.map! { |bv| connection.quote(*bv.reverse) } + collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + collect.substitute_binds(binds).join end end @@ -529,16 +553,22 @@ module ActiveRecord # # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} - def where_values_hash + def where_values_hash(relation_table_name = table_name) equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| - node.left.relation.name == table_name + node.left.relation.name == relation_table_name } binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] Hash[equalities.map { |where| name = where.left.name - [name, binds.fetch(name.to_s) { where.right }] + [name, binds.fetch(name.to_s) { + case where.right + when Array then where.right.map(&:val) + else + where.right.val + end + }] }] end @@ -570,6 +600,8 @@ module ActiveRecord # Compares two relations for equality. def ==(other) case other + when Associations::CollectionProxy, AssociationRelation + self == other.to_a when Relation other.to_sql == to_sql when Array @@ -600,7 +632,7 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) preload = preload_values preload += includes_values unless eager_loading? @@ -617,7 +649,9 @@ module ActiveRecord def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| - unless join.is_a?(Arel::Nodes::StringJoin) + if join.is_a?(Arel::Nodes::StringJoin) + tables_in_string(join.left) + else [join.left.table_name, join.left.table_alias] end end @@ -629,5 +663,12 @@ module ActiveRecord (references_values - joined_tables).any? end + + def tables_in_string(string) + return [] if string.blank? + # always convert table names to downcase as in Oracle quoted table names are in uppercase + # 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/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 29fc150b3d..b069cdce7c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Batches # Looping through a collection of records from the database diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 45ffb99868..90e99957f6 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,6 +19,22 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } + # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value + # of each key would be the +count+. + # + # Article.group(:status, :category).count + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # ["published", "business"]=>0, ["published", "technology"]=>2} + # + # If +count+ is used with +select+, it will count the selected columns: + # + # Person.select(:age).count + # # => counts the number of different age values + # + # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -162,18 +178,7 @@ module ActiveRecord columns_hash.key?(cn) ? arel_table[cn] : cn } result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.map do |attributes| - values = klass.initialize_attributes(attributes).values - - columns.zip(values).map { |column, value| column.type_cast value } - end - columns.one? ? result.map!(&:first) : result + result.cast_values(klass.column_types) end end @@ -188,7 +193,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end def perform_calculation(operation, column_name, options = {}) @@ -231,15 +236,18 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = reorder(nil) + relation = unscope(:order) column_alias = column_name + bind_values = nil + if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) + bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -249,13 +257,14 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel + bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, relation.bind_values) + result = @klass.connection.select_all(query_builder, nil, bind_values) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do - column_for(column_name) + type_for(column_name) end type_cast_calculated_value(value, column, operation) @@ -265,8 +274,8 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.to_sym) - associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + association = @klass._reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs @@ -318,14 +327,14 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do - column_for(col_name) + type_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end @@ -352,24 +361,20 @@ module ActiveRecord @klass.connection.table_alias_for(table_name) end - def column_for(field) + def type_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns_hash[field_name] + @klass.type_for_attribute(field_name) end - def type_cast_calculated_value(value, column, operation = nil) + def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || 0, column) + when 'sum' then type.type_cast_from_database(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type_cast_using_column(value, column) + else type.type_cast_from_database(value) end end - def type_cast_using_column(value, column) - column ? column.type_cast(value) : value - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? @@ -385,9 +390,11 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + arel = relation.arel + subquery = arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine + sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 21beed332f..50f4d5c7ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -40,10 +40,10 @@ module ActiveRecord BLACKLISTED_ARRAY_METHODS = [ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact + :keep_if, :pop, :shift, :delete_at, :compact, :select! ].to_set # :nodoc: - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7099bdd285..0c9c761f97 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module ActiveRecord module FinderMethods ONE_AS_ONE = '1 AS one' @@ -63,7 +65,7 @@ module ActiveRecord # # returns an Array of the required fields, available since Rails 3.1. def find(*args) if block_given? - to_a.find { |*block_args| yield(*block_args) } + to_a.find(*args) { |*block_args| yield(*block_args) } else find_with_ids(*args) end @@ -127,9 +129,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_value, limit) + find_nth_with_limit(offset_index, limit) else - find_nth(:first, offset_value) + find_nth(0, offset_index) end end @@ -179,7 +181,7 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(:second, offset_value ? offset_value + 1 : 1) + find_nth(1, offset_index) end # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -195,7 +197,7 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(:third, offset_value ? offset_value + 2 : 2) + find_nth(2, offset_index) end # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -211,7 +213,7 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(:fourth, offset_value ? offset_value + 3 : 3) + find_nth(3, offset_index) end # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -227,7 +229,7 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(:fifth, offset_value ? offset_value + 4 : 4) + find_nth(4, offset_index) end # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -240,10 +242,10 @@ module ActiveRecord # If no order is defined it will order by primary key. # # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people - # Person.offset(3).forty_two # returns the fifth object from OFFSET 3 (which is OFFSET 44) + # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(:forty_two, offset_value ? offset_value + 41 : 41) + find_nth(41, offset_index) end # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -280,7 +282,12 @@ module ActiveRecord # Person.exists?(false) # Person.exists? def exists?(conditions = :none) - conditions = conditions.id if Base === conditions + if Base === conditions + conditions = conditions.id + ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \ + "Please pass the id of the object by calling `.id`" + end + return false if !conditions relation = apply_join_dependency(self, construct_join_dependency) @@ -292,7 +299,9 @@ module ActiveRecord when Array, Hash relation = relation.where(conditions) else - relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none + unless conditions == :none + relation = where(primary_key => conditions) + end end connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false @@ -322,8 +331,21 @@ module ActiveRecord private + def offset_index + offset_value || 0 + end + def find_with_associations - join_dependency = construct_join_dependency + # NOTE: the JoinDependency constructed here needs to know about + # any joins already present in `self`, so pass them in + # + # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 + # incorrect SQL is generated. In that case, the join dependency for + # SpecialCategorizations is constructed without knowledge of the + # preexisting join in joins_values to categorizations (by way of + # the `has_many :through` for categories). + # + join_dependency = construct_join_dependency(joins_values) aliases = join_dependency.aliases relation = select aliases.columns @@ -335,7 +357,8 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + arel = relation.arel + rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) join_dependency.instantiate(rows, aliases) end end @@ -409,7 +432,11 @@ module ActiveRecord end def find_one(id) - id = id.id if ActiveRecord::Base === id + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ + "Please pass the id of the object by calling `.id`" + end column = columns_hash[primary_key] substitute = connection.substitute_at(column, bind_values.length) @@ -452,20 +479,24 @@ module ActiveRecord end end - def find_nth(ordinal, offset) + def find_nth(index, offset) if loaded? - @records.send(ordinal) + @records[index] else + offset += index @offsets[offset] ||= find_nth_with_limit(offset, 1).first end end def find_nth_with_limit(offset, limit) - if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a - else - limit(limit).offset(offset).to_a - end + relation = if order_values.empty? && primary_key + order(arel_table[primary_key].asc) + else + self + end + + relation = relation.offset(offset) unless offset.zero? + relation.limit(limit).to_a end def find_last diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 182b9ed89c..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -30,6 +30,8 @@ module ActiveRecord else other.joins!(*v) end + elsif k == :select + other._select!(v) else other.send("#{k}!", v) end @@ -62,7 +64,13 @@ module ActiveRecord # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values # don't fall through the cracks. - relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) + unless value.nil? || (value.blank? && false != value) + if name == :select + relation._select!(*value) + else + relation.send("#{name}!", *value) + end + end end merge_multi_values @@ -139,7 +147,6 @@ module ActiveRecord def merge_single_values relation.from_value = values[:from] unless relation.from_value relation.lock_value = values[:lock] unless relation.lock_value - relation.reverse_order_value = values[:reverse_order] unless values[:create_with].blank? relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) @@ -149,7 +156,7 @@ module ActiveRecord def filter_binds(lhs_binds, removed_wheres) return lhs_binds if removed_wheres.empty? - set = Set.new removed_wheres.map { |x| x.left.name } + set = Set.new removed_wheres.map { |x| x.left.name.to_s } lhs_binds.dup.delete_if { |col,_| set.include? col.name } end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1252af7635..eff5c8f09c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord queries << '1=0' else table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) + association = klass._reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) @@ -55,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass.reflect_on_association(column.to_sym) + if klass && reflection = klass._reflect_on_association(column.to_sym) if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) queries << build(table[reflection.foreign_type], base_class) end @@ -113,13 +113,14 @@ module ActiveRecord register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new) - private - def self.build(attribute, value) - handler_for(value).call(attribute, value) - end + def self.build(attribute, value) + handler_for(value).call(attribute, value) + end + private_class_method :build - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end + private_class_method :handler_for end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 2f6c34ac08..78dba8be06 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -2,28 +2,33 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: def call(attribute, value) + return attribute.in([]) if value.empty? + values = value.map { |x| x.is_a?(Base) ? x.id : x } ranges, values = values.partition { |v| v.is_a?(Range) } + nils, values = values.partition(&:nil?) - values_predicate = if values.include?(nil) - values = values.compact - + values_predicate = case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) + when 0 then NullPredicate + when 1 then attribute.eq(values.first) + else attribute.in(values) end - else - attribute.in(values) + + unless nils.empty? + values_predicate = values_predicate.or(attribute.eq(nil)) end array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } end + + module NullPredicate + def self.or(other) + other + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 077f09b67d..1262b2c291 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -49,6 +49,8 @@ module ActiveRecord Arel::Nodes::Not.new(rel) end end + + @scope.references!(PredicateBuilder.references(opts)) if Hash === opts @scope.where_values += where_value @scope end @@ -62,6 +64,7 @@ module ActiveRecord # def #{name}_values=(values) # def select_values=(values) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = values # @values[:select] = values end # end CODE @@ -79,11 +82,22 @@ module ActiveRecord class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = value # @values[:readonly] = value end # end CODE end + def check_cached_relation # :nodoc: + if defined?(@arel) && @arel + @arel = nil + ActiveSupport::Deprecation.warn <<-WARNING +Modifying already cached Relation. The cache will be reset. +Use a cloned Relation to prevent this warning. +WARNING + end + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -168,7 +182,7 @@ module ActiveRecord # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. - # This method only works in conjuction with +includes+. + # This method only works in conjunction with +includes+. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") @@ -202,7 +216,7 @@ module ActiveRecord # fields are retrieved: # # Model.select(:field) - # # => [#<Model field:value>] + # # => [#<Model id: nil, field: "value">] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -211,12 +225,12 @@ module ActiveRecord # The argument to the method can also be an array of fields. # # Model.select(:field, :other_field, :and_one_more) - # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">] # # You can also use one or more strings, which will be used unchanged as SELECT fields. # # Model.select('field AS field_one', 'other_field AS field_two') - # # => [#<Model field: "value", other_field: "value">] + # # => [#<Model id: nil, field: "value", other_field: "value">] # # If an alias was specified, it will be accessible from the resulting objects: # @@ -224,7 +238,7 @@ module ActiveRecord # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select - # will throw <tt>ActiveModel::MissingAttributeError</tt>: + # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field @@ -233,14 +247,14 @@ module ActiveRecord to_a.select { |*block_args| yield(*block_args) } else raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn.select!(*fields) + spawn._select!(*fields) end end - def select!(*fields) # :nodoc: + def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| - klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field + klass.attribute_alias?(field) ? klass.attribute_alias(field) : field end self.select_values += fields self @@ -261,6 +275,10 @@ module ActiveRecord # # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] + # + # Passing in an array of attributes to group by is also supported. + # User.select([:id, :first_name]).group(:id, :first_name).first(3) + # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] def group(*args) check_if_method_has_arguments!(:group, args) spawn.group!(*args) @@ -275,15 +293,6 @@ module ActiveRecord # Allows to specify an order attribute: # - # User.order('name') - # => SELECT "users".* FROM "users" ORDER BY name - # - # User.order('name DESC') - # => SELECT "users".* FROM "users" ORDER BY name DESC - # - # User.order('name DESC, email') - # => SELECT "users".* FROM "users" ORDER BY name DESC, email - # # User.order(:name) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # @@ -292,6 +301,15 @@ module ActiveRecord # # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) check_if_method_has_arguments!(:order, args) spawn.order!(*args) @@ -555,15 +573,11 @@ module ActiveRecord end end - def where!(opts = :chain, *rest) # :nodoc: - if opts == :chain - WhereChain.new(self) - else - references!(PredicateBuilder.references(opts)) if Hash === opts + def where!(opts, *rest) # :nodoc: + references!(PredicateBuilder.references(opts)) if Hash === opts - self.where_values += build_where(opts, rest) - self - end + self.where_values += build_where(opts, rest) + self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. @@ -819,7 +833,9 @@ module ActiveRecord end def reverse_order! # :nodoc: - self.reverse_order_value = !reverse_order_value + orders = order_values.uniq + orders.reject!(&:blank?) + self.order_values = reverse_sql_order(orders) self end @@ -835,7 +851,7 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - ['']).uniq) + collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? @@ -852,6 +868,15 @@ module ActiveRecord arel.from(build_from) if from_value arel.lock(lock_value) if lock_value + # Reorder bind indexes if joins produced bind values + if arel.bind_values.any? + bvs = arel.bind_values + bind_values + arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| + column = bvs[i].first + bp.replace connection.substitute_at(column, i) + end + end + arel end @@ -865,8 +890,9 @@ module ActiveRecord case scope when :order - self.reverse_order_value = false result = [] + when :where + self.bind_values = [] else result = [] unless single_val_method end @@ -917,18 +943,15 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array - #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 - values = Hash === other.first ? other.first.values : other - - values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) - attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + bv_len = bind_values.length + tmp_opts, bind_values = create_binds(opts, bv_len) + self.bind_values += bind_values + + attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end @@ -939,6 +962,29 @@ module ActiveRecord end end + def create_binds(opts, idx) + bindable, non_binds = opts.partition do |column, value| + case value + when String, Integer, ActiveRecord::StatementCache::Substitute + @klass.columns_hash.include? column.to_s + else + false + end + end + + new_opts = {} + binds = [] + + bindable.each_with_index do |(column,value), index| + binds.push [@klass.columns_hash[column.to_s], value] + new_opts[column] = connection.substitute_at(column, index + idx) + end + + non_binds.each { |column,value| new_opts[column] = value } + + [new_opts, binds] + end + def build_from opts, name = from_value case opts @@ -980,9 +1026,12 @@ module ActiveRecord join_list ) - joins = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins - joins.each { |join| manager.from(join) } + join_infos.each do |info| + info.joins.each { |join| manager.from(join) } + manager.bind_values.concat info.binds + end manager.join_sources.concat(join_list) @@ -995,8 +1044,6 @@ module ActiveRecord columns_hash.key?(field.to_s) ? arel_table[field] : field end arel.project(*expanded_select) - elsif from_value - arel.project(Arel.star) else arel.project(@klass.arel_table[Arel.star]) end @@ -1027,15 +1074,19 @@ module ActiveRecord def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) - orders = reverse_sql_order(orders) if reverse_order_value arel.order(*orders) unless orders.empty? end + VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, + 'asc', 'desc', 'ASC', 'DESC'] # :nodoc: + def validate_order_args(args) - args.grep(Hash) do |h| - unless (h.values - [:asc, :desc]).empty? - raise ArgumentError, 'Direction should be :asc or :desc' + args.each do |arg| + next unless arg.is_a?(Hash) + arg.each do |_key, value| + raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ + "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) end end end @@ -1052,12 +1103,12 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arg = klass.attribute_alias(arg).to_sym if klass.attribute_alias?(arg) + arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg) table[arg].asc when Hash arg.map { |field, dir| - field = klass.attribute_alias(field).to_sym if klass.attribute_alias?(field) - table[field].send(dir) + field = klass.attribute_alias(field) if klass.attribute_alias?(field) + table[field].send(dir.downcase) } else arg diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 2552cbd234..57d66bce4b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -58,6 +58,9 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) + if onlies.any? { |o| o == :where } + onlies << :bind + end relation_with values.slice(*onlies) end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 228b2aa60f..8405fdaeb9 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -31,7 +31,7 @@ module ActiveRecord class Result include Enumerable - IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc: + IDENTITY_TYPE = Type::Value.new # :nodoc: attr_reader :columns, :rows, :column_types @@ -42,14 +42,6 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type - end - def each if block_given? hash_rows.each { |row| yield row } @@ -82,6 +74,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +92,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5a71c13d91..ff70cbed0f 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -92,7 +92,7 @@ module ActiveRecord table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) PredicateBuilder.build_from_hash(self, attrs, table).map { |b| - connection.visitor.accept b + connection.visitor.compile b }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -107,6 +107,13 @@ module ActiveRecord end.join(', ') end + # Sanitizes a +string+ so that it is safe to use within an SQL + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].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'" diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 4bfd0167a4..0a5546a760 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Schema # diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..fae6427ea1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -91,16 +91,17 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| - next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(tbl) == ignored - when Regexp; remove_prefix_and_suffix(tbl) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + sorted_tables = @connection.tables.sort + + sorted_tables.each do |table_name| + table(table_name, stream) unless ignored?(table_name) + end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) end - table(tbl, stream) end end @@ -112,7 +113,8 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) + end + if !pk && @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end @@ -212,8 +214,49 @@ HEADER end end + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] + + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << ('column: ' + foreign_key.column.inspect) + end + + if foreign_key.custom_primary_key? + parts << ('primary_key: ' + foreign_key.primary_key.inspect) + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << ('name: ' + foreign_key.name.inspect) + end + + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update + parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete + + ' ' + parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + end + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end + + def ignored?(table_name) + ['schema_migrations', ignore_tables].flatten.any? do |ignored| + case ignored + when String; remove_prefix_and_suffix(table_name) == ignored + when Regexp; remove_prefix_and_suffix(table_name) =~ ignored + else + raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' + end + end + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index a9d164e366..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -5,6 +5,9 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base class << self + def primary_key + nil + end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" @@ -36,6 +39,14 @@ module ActiveRecord connection.drop_table(table_name) end end + + def normalize_migration_number(number) + "%.3d" % number.to_i + end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 01fec31544..18190cb535 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -11,7 +11,7 @@ module ActiveRecord end module ClassMethods - # Returns a scope for the model without the +default_scope+. + # Returns a scope for the model without the previously set scopes. # # class Post < ActiveRecord::Base # def self.default_scope @@ -19,11 +19,12 @@ module ActiveRecord # end # end # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts" # # This method also accepts a block. All queries inside the block will - # not use the +default_scope+: + # not use the previously set scopes. # # Post.unscoped { # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" @@ -93,14 +94,14 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope # :nodoc: + def build_default_scope(base_rel = relation) # :nodoc: if !Base.is_a?(method(:default_scope).owner) # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do - default_scopes.inject(relation) do |default_scope, scope| - default_scope.merge(unscoped { scope.call }) + default_scopes.inject(base_rel) do |default_scope, scope| + default_scope.merge(base_rel.scoping { scope.call }) end end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 1a766093d0..c2484d02ed 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,13 +180,9 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - type = if klass.serialized_attributes.key?(name) - super - elsif klass.columns_hash.key?(name) - klass.columns_hash[name].type - else - NilClass - end + column = klass.columns_hash[name] || Type::Value.new + + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index dd4ee0c4a0..aece446384 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -14,13 +14,87 @@ module ActiveRecord # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. # Database is queried when +to_a+ is called on the relation. class StatementCache - def initialize - @relation = yield - raise ArgumentError.new("Statement cannot be nil") if @relation.nil? + class Substitute; end + + class Query + def initialize(sql) + @sql = sql + end + + def sql_for(binds, connection) + @sql + end + end + + class PartialQuery < Query + def initialize values + @values = values + @indexes = values.each_with_index.find_all { |thing,i| + Arel::Nodes::BindParam === thing + }.map(&:last) + end + + def sql_for(binds, connection) + val = @values.dup + binds = binds.dup + @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + val.join + end + end + + def self.query(visitor, ast) + Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value + end + + def self.partial_query(visitor, ast, collector) + collected = visitor.accept(ast, collector).value + PartialQuery.new collected + end + + class Params + def bind; Substitute.new; end end - def execute - @relation.dup.to_a + class BindMap + def initialize(bind_values) + @indexes = [] + @bind_values = bind_values + + bind_values.each_with_index do |(_, value), i| + if Substitute === value + @indexes << i + end + end + end + + def bind(values) + bvs = @bind_values.map { |pair| pair.dup } + @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } + bvs + end + end + + attr_reader :bind_map, :query_builder + + def self.create(connection, block = Proc.new) + relation = block.call Params.new + bind_map = BindMap.new relation.bind_values + query_builder = connection.cacheable_query relation.arel + new query_builder, bind_map + end + + def initialize(query_builder, bind_map) + @query_builder = query_builder + @bind_map = bind_map + end + + def execute(params, klass, connection) + bind_values = bind_map.bind params + + sql = query_builder.sql_for bind_values, connection + + klass.find_by_sql sql, bind_values end + alias :call :execute end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 79a6ccbda0..3c291f28e3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -66,8 +66,9 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_accessor: false - self.stored_attributes = {} + class << self + attr_accessor :local_stored_attributes + end end module ClassMethods @@ -93,18 +94,26 @@ module ActiveRecord # assign new store attribute and create new hash to ensure that each class in the hierarchy # has its own hash of stored attributes. - self.stored_attributes = {} if self.stored_attributes.blank? - self.stored_attributes[store_attribute] ||= [] - self.stored_attributes[store_attribute] |= keys + self.local_stored_attributes ||= {} + self.local_stored_attributes[store_attribute] ||= [] + self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod mod end end + + def stored_attributes + parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {} + if self.local_stored_attributes + parent.merge!(self.local_stored_attributes) { |k, a, b| a | b } + end + parent + end end protected @@ -120,10 +129,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -142,7 +151,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -152,7 +161,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 6ce0495f6f..892c78e479 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -6,7 +6,7 @@ module ActiveRecord # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used in rake tasks provided by Active Record. + # The tasks defined here are used with Rake tasks provided by Active Record. # # In order to use DatabaseTasks, a few config values need to be set. All the needed # config values are set by Rails already, so it's necessary to do it only if you @@ -14,7 +14,6 @@ module ActiveRecord # (in such case after configuring the database tasks, you can also use the rake tasks # defined in Active Record). # - # # The possible config values are: # # * +env+: current environment (like Rails.env). @@ -28,7 +27,7 @@ module ActiveRecord # Example usage of +DatabaseTasks+ outside Rails could look as such: # # include ActiveRecord::Tasks - # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml')) + # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') # DatabaseTasks.db_dir = 'db' # # other settings... # @@ -59,7 +58,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -107,6 +110,8 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue ActiveRecord::NoDatabaseError + $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't drop #{configuration['database']}" @@ -122,6 +127,16 @@ module ActiveRecord } end + def migrate + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + scope = ENV['SCOPE'] + Migration.verbose = verbose + Migrator.migrate(Migrator.migrations_paths, version) do |migration| + scope.blank? || scope == migration.scope + end + end + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -144,6 +159,18 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(configuration).purge end + def purge_all + each_local_configuration { |configuration| + purge configuration + } + end + + def purge_current(environment = env) + each_current_configuration(environment) { |configuration| + purge configuration + } + end + def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 @@ -157,20 +184,39 @@ module ActiveRecord end def load_schema(format = ActiveRecord::Base.schema_format, file = nil) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + This method will act on a specific connection in the future. + To act on the current connection, use `load_schema_current` instead. + MESSAGE + load_schema_current(format, file) + end + + # This method is the successor of +load_schema+. We should rename it + # after +load_schema+ went through a deprecation cycle. (Rails > 4.2) + def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: case format when :ruby file ||= File.join(db_dir, "schema.rb") check_schema_file(file) + purge(configuration) + ActiveRecord::Base.establish_connection(configuration) load(file) when :sql file ||= File.join(db_dir, "structure.sql") check_schema_file(file) - structure_load(current_config, file) + purge(configuration) + structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + each_current_configuration(environment) { |configuration| + load_schema_for configuration, format, file + } + end + def check_schema_file(filename) unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..d890196f47 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -42,7 +42,7 @@ module ActiveRecord end def purge - establish_connection :test + establish_connection configuration connection.recreate_database configuration['database'], creation_options end @@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 3d02ee07d0..ce1de4b76e 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -54,7 +54,7 @@ module ActiveRecord command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" raise 'Error dumping database' unless Kernel.system(command) - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 5688931db2..9ab64d0325 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -21,7 +21,11 @@ module ActiveRecord FileUtils.rm(file) if File.exist?(file) end - alias :purge :drop + + def purge + drop + create + end def charset connection.encoding diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 271f58cbe0..5ef98ed820 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Timestamp # @@ -43,12 +42,12 @@ module ActiveRecord private - def create_record + def _create_record if self.record_timestamps current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - if attributes.has_key?(column.to_s) && self.send(column).nil? + if attributes.key?(column.to_s) && self.send(column).nil? write_attribute(column.to_s, current_time) end end @@ -57,7 +56,7 @@ module ActiveRecord super end - def update_record(*args) + def _update_record(*args) if should_record_timestamps? current_time = current_time_from_proper_timezone @@ -99,9 +98,11 @@ module ActiveRecord end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) - if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present? - timestamps.map { |ts| ts.to_time }.max - end + timestamp_names + .map { |attr| self[attr] } + .compact + .map(&:to_time) + .max end def current_time_from_proper_timezone diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ec3e8f281b..7e4dc4c895 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,5 +1,3 @@ -require 'thread' - module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions @@ -295,7 +293,7 @@ module ActiveRecord def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure - @_start_transaction_state.clear + force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record @@ -328,7 +326,7 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + clear_transaction_record_state status = nil end @@ -341,7 +339,7 @@ module ActiveRecord # 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: - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:id] = id unless @_start_transaction_state.include?(:new_record) @_start_transaction_state[:new_record] = @new_record end @@ -349,13 +347,18 @@ module ActiveRecord @_start_transaction_state[:destroyed] = @destroyed end @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = @attributes.frozen? + @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 + force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 + end + + # Force to clear the transaction record state. + def force_clear_transaction_record_state #:nodoc: + @_start_transaction_state.clear end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. @@ -364,17 +367,10 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - was_frozen = restore_state[:frozen?] - @attributes = @attributes.dup if @attributes.frozen? + thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - if restore_state.has_key?(:id) - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end - @attributes.freeze if was_frozen + write_attribute(self.class.primary_key, restore_state[:id]) end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb new file mode 100644 index 0000000000..f1384e0bb2 --- /dev/null +++ b/activerecord/lib/active_record/type.rb @@ -0,0 +1,20 @@ +require 'active_record/type/mutable' +require 'active_record/type/numeric' +require 'active_record/type/time_value' +require 'active_record/type/value' + +require 'active_record/type/binary' +require 'active_record/type/boolean' +require 'active_record/type/date' +require 'active_record/type/date_time' +require 'active_record/type/decimal' +require 'active_record/type/decimal_without_scale' +require 'active_record/type/float' +require 'active_record/type/integer' +require 'active_record/type/serialized' +require 'active_record/type/string' +require 'active_record/type/text' +require 'active_record/type/time' + +require 'active_record/type/type_map' +require 'active_record/type/hash_lookup_type_map' diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb new file mode 100644 index 0000000000..d29ff4e494 --- /dev/null +++ b/activerecord/lib/active_record/type/binary.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def type_cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def type_cast_for_database(value) + return if value.nil? + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value.to_s + end + + def to_s + @value + end + + def hex + @value.unpack('H*')[0] + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb new file mode 100644 index 0000000000..06dd17ed28 --- /dev/null +++ b/activerecord/lib/active_record/type/boolean.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Boolean < Value # :nodoc: + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + ConnectionAdapters::Column::TRUE_VALUES.include?(value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb new file mode 100644 index 0000000000..d90a6069b7 --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return 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 fast_string_to_date(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb new file mode 100644 index 0000000000..5f19608a33 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module Type + class DateTime < Value # :nodoc: + include TimeValue + + def type + :datetime + end + + def type_cast_for_database(value) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.acts_like?(:time) + value.send(zone_conversion_method) + else + super + end + end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb new file mode 100644 index 0000000000..d10778eeb6 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s + end + + private + + def cast_value(value) + case value + when ::Float + BigDecimal(value, float_precision) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 + else + precision.to_i + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..cabdcecdd7 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb new file mode 100644 index 0000000000..42eb44b9a9 --- /dev/null +++ b/activerecord/lib/active_record/type/float.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + value.to_f + end + end + end +end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..bf92680268 --- /dev/null +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type, *args) + @mapping.fetch(type, proc { default_value }).call(type, *args) + end + + def fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb new file mode 100644 index 0000000000..08477d1303 --- /dev/null +++ b/activerecord/lib/active_record/type/integer.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb new file mode 100644 index 0000000000..066617ea59 --- /dev/null +++ b/activerecord/lib/active_record/type/mutable.rb @@ -0,0 +1,16 @@ +module ActiveRecord + module Type + module Mutable # :nodoc: + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != type_cast_for_database(new_value) + end + end + end +end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb new file mode 100644 index 0000000000..fa43266504 --- /dev/null +++ b/activerecord/lib/active_record/type/numeric.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A\d+\.?\d*\z/ + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb new file mode 100644 index 0000000000..abeea769c4 --- /dev/null +++ b/activerecord/lib/active_record/type/serialized.rb @@ -0,0 +1,51 @@ +module ActiveRecord + module Type + class Serialized < SimpleDelegator # :nodoc: + include Mutable + + attr_reader :subtype, :coder + + def initialize(subtype, coder) + @subtype = subtype + @coder = coder + super(subtype) + end + + def type_cast_from_database(value) + if default_value?(value) + value + else + coder.load(super) + end + end + + def type_cast_for_database(value) + return if value.nil? + unless default_value?(value) + super coder.dump(value) + end + end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end + + def init_with(coder) + @subtype = coder['subtype'] + @coder = coder['coder'] + __setobj__(@subtype) + end + + def encode_with(coder) + coder['subtype'] = @subtype + coder['coder'] = @coder + end + + private + + def default_value?(value) + value == coder.load(nil) + end + end + end +end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb new file mode 100644 index 0000000000..150defb106 --- /dev/null +++ b/activerecord/lib/active_record/type/string.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module Type + class String < Value # :nodoc: + def type + :string + end + + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def type_cast_for_database(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + # String.new is slightly faster than dup + else ::String.new(value.to_s) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..26f980f060 --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,11 @@ +require 'active_record/type/string' + +module ActiveRecord + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb new file mode 100644 index 0000000000..41f7d97f0c --- /dev/null +++ b/activerecord/lib/active_record/type/time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module Type + class Time < Value # :nodoc: + include TimeValue + + def type + :time + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb new file mode 100644 index 0000000000..d611d72dd4 --- /dev/null +++ b/activerecord/lib/active_record/type/time_value.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb new file mode 100644 index 0000000000..88c5f9c497 --- /dev/null +++ b/activerecord/lib/active_record/type/type_map.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + end + + def lookup(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + default_value + end + end + + def register_type(key, value = nil, &block) + raise ::ArgumentError unless value || block + + if block + @mapping[key] = block + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + private + + def default_value + @default_value ||= Value.new + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb new file mode 100644 index 0000000000..475e130013 --- /dev/null +++ b/activerecord/lib/active_record/type/value.rb @@ -0,0 +1,94 @@ +module ActiveRecord + module Type + class Value # :nodoc: + attr_reader :precision, :scale, :limit + + # Valid options are +precision+, +scale+, and +limit+. They are only + # used when dumping schema. + def initialize(options = {}) + options.assert_valid_keys(:precision, :scale, :limit) + @precision = options[:precision] + @scale = options[:scale] + @limit = options[:limit] + end + + # The simplified type that this object represents. Returns a symbol such + # as +:string+ or +:integer+ + def type; end + + # Type casts a string from the database into the appropriate ruby type. + # Classes which do not need separate type casting behavior for database + # and user provided values should override +cast_value+ instead. + def type_cast_from_database(value) + type_cast(value) + end + + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or an already type cast value + # provided manually to a setter. + # + # Classes which do not need separate type casting behavior for database + # and user provided values should override +type_cast+ or +cast_value+ + # instead. + def type_cast_from_user(value) + type_cast(value) + end + + # Cast a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+ + def type_cast_for_database(value) + value + end + + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: + value.inspect + end + + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def number? # :nodoc: + false + end + + def binary? # :nodoc: + false + end + + def klass # :nodoc: + end + + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value + end + + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. This method should not be overridden + # directly. Types which return a mutable value should include + # +Type::Mutable+, which will define this method. + def changed_in_place?(*) + false + end + + private + + def type_cast(value) + cast_value(value) unless value.nil? + end + + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by + # `type_cast_from_database` and `type_cast_from_user` for all values + # except `nil`. + def cast_value(value) # :doc: + value + end + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 26dca415ff..7f7d49cdb4 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -29,21 +29,6 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::Validations - module ClassMethods - # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ - # so an exception is raised if the record is invalid. - def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - object = new(attributes) - yield(object) if block_given? - object.save! - object - end - end - end - # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular Base#save method is replaced with this when the validations # module is mixed in, which it is by default. @@ -54,12 +39,14 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if # no errors are found, +false+ otherwise. # + # Aliased as validate. + # # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. # @@ -71,8 +58,26 @@ module ActiveRecord errors.empty? && output end + alias_method :validate, :valid? + + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 9a19483da3..e586744818 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -4,7 +4,7 @@ module ActiveRecord def validate(record) super attributes.each do |attribute| - next unless record.class.reflect_on_association(attribute) + next unless record.class._reflect_on_association(attribute) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 7ebe9dfec0..2dba4c7b94 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -13,7 +13,7 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) table = finder_class.arel_table - value = deserialize_attribute(record, attribute, value) + value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? @@ -46,9 +46,9 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - if reflection = klass.reflect_on_association(attribute) + if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] unless value.nil? + value = value.attributes[reflection.klass.primary_key] unless value.nil? end attribute_name = attribute.to_s @@ -61,20 +61,21 @@ module ActiveRecord column = klass.columns_hash[attribute_name] value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end - if !options[:case_sensitive] && value && column.text? + if !options[:case_sensitive] && value.is_a?(String) # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else - value = klass.connection.case_sensitive_modifier(value) unless value.nil? - table[attribute].eq(value) + klass.connection.case_sensitive_comparison(table, attribute, column, value) end end def scope_relation(record, table, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class.reflect_on_association(scope_item) + if reflection = record.class._reflect_on_association(scope_item) scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else @@ -86,9 +87,9 @@ module ActiveRecord relation end - def deserialize_attribute(record, attribute, value) - coder = record.class.serialized_attributes[attribute.to_s] - value = coder.dump value if value && coder + def map_enum_attribute(klass, attribute, value) + mapping = klass.defined_enums[attribute.to_s] + value = mapping[value] if value && mapping value end end @@ -151,7 +152,7 @@ module ActiveRecord # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to - # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>, + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 863c3ebe4d..cf76a13b44 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveRecord - # Returns the version of the currently loaded ActiveRecord as a Gem::Version + # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.1.0.beta1" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments - STRING = ActiveRecord.version.to_s + gem_version end end 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 3968acba64..d3c853cfea 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -23,16 +23,16 @@ module ActiveRecord case file_name when /^(add|remove)_.*_(?:to|from)_(.*)/ @migration_action = $1 - @table_name = $2.pluralize + @table_name = normalize_table_name($2) when /join_table/ if attributes.length == 2 @migration_action = 'join' - @join_tables = attributes.map(&:plural_name) + @join_tables = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name) set_index_names end when /^create_(.+)/ - @table_name = $1.pluralize + @table_name = normalize_table_name($1) @migration_template = "create_table_migration.rb" end end @@ -61,6 +61,10 @@ module ActiveRecord raise IllegalMigrationNameError.new(file_name) end end + + def normalize_table_name(_table_name) + pluralize_table_names? ? _table_name.pluralize : _table_name.singularize + end end end end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 59324c4857..64cde143a1 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -29,6 +29,7 @@ module ActiveRecord @columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new( name.to_s, options[:default], + lookup_cast_type(sql_type.to_s), sql_type.to_s, options[:null]) end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 0eb1231c79..6f84bae432 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -46,9 +46,7 @@ module ActiveRecord @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_equal idx_name, indexes.first.name assert !indexes.first.unique assert_equal ["firm_id"], indexes.first.columns else @@ -127,14 +125,12 @@ module ActiveRecord 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! } - end + 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 @@ -145,8 +141,8 @@ module ActiveRecord end end - def test_foreign_key_violations_are_translated_to_specific_exception - unless @connection.adapter_name == 'SQLite' + unless current_adapter?(:SQLite3Adapter) + def test_foreign_key_violations_are_translated_to_specific_exception 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? @@ -157,6 +153,18 @@ module ActiveRecord end end end + + def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false + klass_has_fk = Class.new(ActiveRecord::Base) do + self.table_name = 'fk_test_has_fk' + end + + assert_raises(ActiveRecord::InvalidForeignKey) do + has_fk = klass_has_fk.new + has_fk.fk_id = 1231231231 + has_fk.save(validate: false) + end + end end def test_disable_referential_integrity @@ -184,7 +192,7 @@ module ActiveRecord def test_select_methods_passing_a_association_relation author = Author.create!(name: 'john') Post.create!(author: author, title: 'foo', body: 'bar') - query = author.posts.select(:title) + query = author.posts.where(title: 'foo').select(:title) assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) @@ -218,7 +226,7 @@ module ActiveRecord @connection = Klass.connection end - def teardown + teardown do Klass.remove_connection end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 0878925a6c..7c0f11b033 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -1,23 +1,23 @@ require "cases/helper" +require 'support/connection_helper' class ActiveSchemaTest < ActiveRecord::TestCase - def setup - @connection = ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection) + include ConnectionHelper + def setup ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute def execute(sql, name = nil) return sql end end end - def teardown - ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection) + teardown do + reset_connection end def test_add_index - # add_index calls index_name_exists? which can't work since execute is stubbed + # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).table_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -116,6 +116,18 @@ class ActiveSchemaTest < ActiveRecord::TestCase end end + def test_indexes_in_create + ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) + + expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| + t.index :zip + end + + assert_equal expected, actual + end + private def with_real_execute ActiveRecord::Base.connection.singleton_class.class_eval do diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 97adb6b297..340fc95503 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -3,10 +3,10 @@ require 'models/person' class MysqlCaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base - validates_uniqueness_of :string_cs_column, :case_sensitive => false - validates_uniqueness_of :string_ci_column, :case_sensitive => false end + repair_validations(CollationTest) + def test_columns_include_collation_different_from_table assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation @@ -18,6 +18,7 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) CollationTest.create!(:string_ci_column => 'A') invalid = CollationTest.new(:string_ci_column => 'a') queries = assert_sql { invalid.save } @@ -26,10 +27,29 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) CollationTest.create!(:string_cs_column => 'A') invalid = CollationTest.new(:string_cs_column => 'a') queries = assert_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_match(/lower/i, cs_uniqueness_query) end + + def test_case_sensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'A') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } + assert_match(/binary/i, ci_uniqueness_query) + end + + def test_case_sensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'A') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } + assert_no_match(/binary/i, cs_uniqueness_query) + end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 5cd5d8ac5f..a7b0addc1b 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -1,6 +1,11 @@ require "cases/helper" +require 'support/connection_helper' +require 'support/ddl_helper' class MysqlConnectionTest < ActiveRecord::TestCase + include ConnectionHelper + include DdlHelper + class Klass < ActiveRecord::Base end @@ -69,59 +74,50 @@ class MysqlConnectionTest < ActiveRecord::TestCase end def test_exec_no_binds - @connection.exec_query('drop table if exists ex') - @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, - `data` varchar(255)) - eosql - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns + with_example_table do + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + # if there are no bind parameters, it will return a string (due to + # the libmysql api) + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [['1', 'foo']], result.rows + end end def test_exec_with_binds - @connection.exec_query('drop table if exists ex') - @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, - `data` varchar(255)) - eosql - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + with_example_table do + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [[1, 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end def test_exec_typecasts_bind_vals - @connection.exec_query('drop table if exists ex') - @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, - `data` varchar(255)) - eosql - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - column = @connection.columns('ex').find { |col| col.name == 'id' } + with_example_table do + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @connection.columns('ex').find { |col| col.name == 'id' } - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [[1, 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end # Test that MySQL allows multiple results for stored procedures @@ -138,12 +134,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -155,6 +150,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_sql_mode_variable_overrides_strict_mode + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) + result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' + assert_not_equal [['STRICT_ALL_TABLES']], result.rows + end + end + def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) @@ -166,12 +169,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase private - def run_without_connection - original_connection = ActiveRecord::Base.remove_connection - begin - yield original_connection - ensure - ActiveRecord::Base.establish_connection(original_connection) - end + def with_example_table(&block) + definition ||= <<-SQL + `id` int(11) auto_increment PRIMARY KEY, + `data` varchar(255) + SQL + super(@connection, 'ex', definition, &block) end end diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb new file mode 100644 index 0000000000..083d533bb2 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -0,0 +1,48 @@ +require "cases/helper" + +class MysqlConsistencyTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Consistency < ActiveRecord::Base + self.table_name = "mysql_consistency" + end + + setup do + @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans + ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false + + @connection = ActiveRecord::Base.connection + @connection.create_table("mysql_consistency") do |t| + t.boolean "a_bool" + t.string "a_string" + end + Consistency.reset_column_information + Consistency.create! + end + + teardown do + ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans + @connection.drop_table "mysql_consistency" + end + + test "boolean columns with random value type cast to 0 when emulate_booleans is false" do + with_new = Consistency.new + with_last = Consistency.last + with_new.a_bool = 'wibble' + with_last.a_bool = 'wibble' + + assert_equal 0, with_new.a_bool + assert_equal 0, with_last.a_bool + end + + test "string columns call #to_s" do + with_new = Consistency.new + with_last = Consistency.last + thing = Object.new + with_new.a_string = thing + with_last.a_string = thing + + assert_equal thing.to_s, with_new.a_string + assert_equal thing.to_s, with_last.a_string + 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 578f6301bd..28106d3772 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -1,19 +1,15 @@ # encoding: utf-8 require "cases/helper" +require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters class MysqlAdapterTest < ActiveRecord::TestCase + include DdlHelper + def setup @conn = ActiveRecord::Base.connection - @conn.exec_query('drop table if exists ex') - @conn.exec_query(<<-eosql) - CREATE TABLE `ex` ( - `id` int(11) auto_increment PRIMARY KEY, - `number` integer, - `data` varchar(255)) - eosql end def test_bad_connection_mysql @@ -25,8 +21,10 @@ module ActiveRecord end def test_valid_column - column = @conn.columns('ex').find { |col| col.name == 'id' } - assert @conn.valid_type?(column.type) + with_example_table do + column = @conn.columns('ex').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end end def test_invalid_column @@ -38,31 +36,35 @@ module ActiveRecord end def test_exec_insert_number - insert(@conn, 'number' => 10) + with_example_table do + insert(@conn, 'number' => 10) - result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') + result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') - assert_equal 1, result.rows.length - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - assert_equal '10', result.rows.last.last + assert_equal 1, result.rows.length + # if there are no bind parameters, it will return a string (due to + # the libmysql api) + assert_equal '10', result.rows.last.last + end end def test_exec_insert_string - str = 'いただきます!' - insert(@conn, 'number' => 10, 'data' => str) + with_example_table do + str = 'いただきます!' + insert(@conn, 'number' => 10, 'data' => str) - result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') + result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') - value = result.rows.last.last + value = result.rows.last.last - # 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) + # The strings in this file are utf-8, so transcode to utf-8 + value.encode!(Encoding::UTF_8) - assert_equal str, value + assert_equal str, value + end end def test_tables_quoting @@ -74,46 +76,37 @@ module ActiveRecord end def test_pk_and_sequence_for - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq + with_example_table do + pk, seq = @conn.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex', 'id'), seq + end end def test_pk_and_sequence_for_with_non_standard_primary_key - @conn.exec_query('drop table if exists ex_with_non_standard_pk') - @conn.exec_query(<<-eosql) - CREATE TABLE `ex_with_non_standard_pk` ( - `code` INT(11) auto_increment, - PRIMARY KEY (`code`)) - eosql - pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk') - assert_equal 'code', pk - assert_equal @conn.default_sequence_name('ex_with_non_standard_pk', 'code'), seq + with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do + pk, seq = @conn.pk_and_sequence_for('ex') + assert_equal 'code', pk + assert_equal @conn.default_sequence_name('ex', 'code'), seq + end end def test_pk_and_sequence_for_with_custom_index_type_pk - @conn.exec_query('drop table if exists ex_with_custom_index_type_pk') - @conn.exec_query(<<-eosql) - CREATE TABLE `ex_with_custom_index_type_pk` ( - `id` INT(11) auto_increment, - PRIMARY KEY USING BTREE (`id`)) - eosql - pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq + with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do + pk, seq = @conn.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex', 'id'), seq + end end def test_tinyint_integer_typecasting - @conn.exec_query('drop table if exists ex_with_non_boolean_tinyint_column') - @conn.exec_query(<<-eosql) - CREATE TABLE `ex_with_non_boolean_tinyint_column` ( - `status` TINYINT(4)) - eosql - insert(@conn, { 'status' => 2 }, 'ex_with_non_boolean_tinyint_column') + with_example_table '`status` TINYINT(4)' do + insert(@conn, { 'status' => 2 }, 'ex') - result = @conn.exec_query('SELECT status FROM ex_with_non_boolean_tinyint_column') + result = @conn.exec_query('SELECT status FROM ex') - assert_equal 2, result.column_types['status'].type_cast(result.last['status']) + assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status']) + end end def test_supports_extensions @@ -140,6 +133,15 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end + + def with_example_table(definition = nil, &block) + definition ||= <<-SQL + `id` int(11) auto_increment PRIMARY KEY, + `number` integer, + `data` varchar(255) + SQL + super(@conn, 'ex', definition, &block) + end end end end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index 3d1330efb8..d8a954efa8 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,13 +9,13 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, 'boolean') + c = Column.new(nil, 1, Type::Boolean.new) assert_equal 1, @conn.type_cast(true, nil) assert_equal 1, @conn.type_cast(true, c) end def test_type_cast_false - c = Column.new(nil, 1, 'boolean') + c = Column.new(nil, 1, Type::Boolean.new) assert_equal 0, @conn.type_cast(false, nil) assert_equal 0, @conn.type_cast(false, c) end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 8eb9565963..61ae0abfd1 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'distinct_select'=>'distinct_id int, select_id int' end - def teardown + teardown do drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 807a7a155e..87c5277e64 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -17,6 +17,44 @@ module ActiveRecord self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end + + @connection.create_table "mysql_doubles" + end + + teardown do + @connection.execute "drop table if exists mysql_doubles" + end + + class MysqlDouble < ActiveRecord::Base + self.table_name = "mysql_doubles" + end + + def test_float_limits + @connection.add_column :mysql_doubles, :float_no_limit, :float + @connection.add_column :mysql_doubles, :float_short, :float, limit: 5 + @connection.add_column :mysql_doubles, :float_long, :float, limit: 53 + + @connection.add_column :mysql_doubles, :float_23, :float, limit: 23 + @connection.add_column :mysql_doubles, :float_24, :float, limit: 24 + @connection.add_column :mysql_doubles, :float_25, :float, limit: 25 + MysqlDouble.reset_column_information + + column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } + column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } + column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } + + column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } + column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } + column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } + + # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 + assert_equal 24, column_no_limit.limit + assert_equal 24, column_short.limit + assert_equal 53, column_long.limit + + assert_equal 24, column_23.limit + assert_equal 24, column_24.limit + assert_equal 53, column_25.limit end def test_schema diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 4ccf568406..cefc3e3c7e 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,23 +1,23 @@ require "cases/helper" +require 'support/connection_helper' class ActiveSchemaTest < ActiveRecord::TestCase - def setup - @connection = ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection) + include ConnectionHelper + def setup ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute def execute(sql, name = nil) return sql end end end - def teardown - ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection) + teardown do + reset_connection end def test_add_index - # add_index calls index_name_exists? which can't work since execute is stubbed + # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).table_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -116,6 +116,18 @@ class ActiveSchemaTest < ActiveRecord::TestCase end end + def test_indexes_in_create + ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) + + expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| + t.index :zip + end + + assert_equal expected, actual + end + private def with_real_execute ActiveRecord::Base.connection.singleton_class.class_eval do diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 267aa232d9..f3c711a64b 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "test type casting without emulated booleans" do @@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 6bcc113482..09bebf3071 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -3,10 +3,10 @@ require 'models/person' class Mysql2CaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base - validates_uniqueness_of :string_cs_column, :case_sensitive => false - validates_uniqueness_of :string_ci_column, :case_sensitive => false end + repair_validations(CollationTest) + def test_columns_include_collation_different_from_table assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation @@ -18,6 +18,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) CollationTest.create!(:string_ci_column => 'A') invalid = CollationTest.new(:string_ci_column => 'a') queries = assert_sql { invalid.save } @@ -26,10 +27,29 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) CollationTest.create!(:string_cs_column => 'A') invalid = CollationTest.new(:string_cs_column => 'a') queries = assert_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)} assert_match(/lower/i, cs_uniqueness_query) end + + def test_case_sensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'A') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } + assert_match(/binary/i, ci_uniqueness_query) + end + + def test_case_sensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'A') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } + assert_no_match(/binary/i, cs_uniqueness_query) + end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 9b7202c915..beedb4f3a1 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,15 +1,18 @@ require "cases/helper" +require 'support/connection_helper' class MysqlConnectionTest < ActiveRecord::TestCase + include ConnectionHelper + def setup super @subscriber = SQLSubscriber.new - ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) + @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection end def teardown - ActiveSupport::Notifications.unsubscribe(@subscriber) + ActiveSupport::Notifications.unsubscribe(@subscription) super end @@ -57,12 +60,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -74,6 +76,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_sql_mode_variable_overrides_strict_mode + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) + result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' + assert_not_equal [['STRICT_ALL_TABLES']], result.rows + end + end + def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) @@ -97,14 +107,10 @@ class MysqlConnectionTest < ActiveRecord::TestCase @connection.execute "DROP TABLE `bar_baz`" end - private - - def run_without_connection - original_connection = ActiveRecord::Base.remove_connection - begin - yield original_connection - ensure - ActiveRecord::Base.establish_connection(original_connection) + if mysql_56? + def test_quote_time_usec + assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0)) + assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime) end end end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 1cd356e868..675703caa1 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -9,15 +9,15 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain end def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 1a82308176..799d927ee4 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'distinct_select'=>'distinct_id int, select_id int' end - def teardown + teardown do drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index ec73ec35aa..9c49599d34 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -6,12 +6,12 @@ module ActiveRecord class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" - connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) ensure - connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars" + connection.remove_foreign_key :engines, name: "fk_engines_cars" end def test_initializes_schema_migrations_for_encoding_utf8mb4 diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 22dd48e113..3808db5141 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -7,7 +7,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase end end - def teardown + teardown do ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do remove_method :execute end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 3090f4478f..8df1b7d18c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,32 +1,45 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlArrayTest < ActiveRecord::TestCase + include InTimeZone + OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' end def setup @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + @connection.commit_db_transaction + end + + @connection.reconnect! + @connection.transaction do @connection.create_table('pg_arrays') do |t| t.string 'tags', array: true t.integer 'ratings', array: true + t.datetime :datetimes, array: true + t.hstore :hstores, array: true end end - @column = PgArray.columns.find { |c| c.name == 'tags' } + @column = PgArray.columns_hash['tags'] end - def teardown + teardown do @connection.execute 'drop table if exists pg_arrays' end def test_column assert_equal :string, @column.type + assert_equal "character varying", @column.sql_type assert @column.array - assert_not @column.text? + assert_not @column.number? + assert_not @column.binary? ratings_column = PgArray.columns_hash['ratings'] assert_equal :integer, ratings_column.type @@ -34,15 +47,35 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_not ratings_column.number? end + def test_default + @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2] + PgArray.reset_column_information + + assert_equal([4, 4, 2], PgArray.column_defaults['score']) + assert_equal([4, 4, 2], PgArray.new.score) + ensure + PgArray.reset_column_information + end + + def test_default_strings + @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"] + PgArray.reset_column_information + + assert_equal(["foo", "bar"], PgArray.column_defaults['names']) + assert_equal(["foo", "bar"], PgArray.new.names) + ensure + PgArray.reset_column_information + end + def test_change_column_with_array @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] - @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}" + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [] PgArray.reset_column_information - column = PgArray.columns.find { |c| c.name == 'snippets' } + column = PgArray.columns_hash['snippets'] assert_equal :text, column.type - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['snippets'] assert column.array end @@ -55,38 +88,56 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end end - def test_type_cast_array - data = '{1,2,3}' - oid_type = @column.instance_variable_get('@oid_type').subtype - # we are getting the instance variable in this test, but in the - # normal use of string_to_array, it's called from the OID::Array - # class and will have the OID instance that will provide the type - # casting - array = @column.class.string_to_array data, oid_type - assert_equal(['1', '2', '3'], array) - assert_equal(['1', '2', '3'], @column.type_cast(data)) + def test_change_column_default_with_array + @connection.change_column_default :pg_arrays, :tags, [] - assert_equal([], @column.type_cast('{}')) - assert_equal([nil], @column.type_cast('{NULL}')) + PgArray.reset_column_information + assert_equal [], PgArray.column_defaults['tags'] + end + + def test_type_cast_array + assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}')) + assert_equal([], @column.type_cast_from_database('{}')) + assert_equal([nil], @column.type_cast_from_database('{NULL}')) end def test_type_cast_integers x = PgArray.new(ratings: ['1', '2']) - assert x.save! - assert_equal(['1', '2'], x.ratings) + + assert_equal([1, 2], x.ratings) + + x.save! + x.reload + + assert_equal([1, 2], x.ratings) end - def test_rewrite + def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - x.tags = ['1','2','3','4'] - assert x.save! + assert_equal(['1','2','3'], x.tags) end - def test_select + def test_rewrite_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - assert_equal(['1','2','3'], x.tags) + x.tags = ['1','2','3','4'] + x.save! + assert_equal ['1','2','3','4'], x.reload.tags + end + + def test_select_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal([1, 2, 3], x.ratings) + end + + def test_rewrite_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + x.ratings = [2, '3', 4] + x.save! + assert_equal [2, 3, 4], x.reload.ratings end def test_multi_dimensional_with_strings @@ -140,14 +191,71 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end - def test_update_all - pg_array = PgArray.create! tags: ["one", "two", "three"] + def test_escaping + unknown = 'foo\\",bar,baz,\\' + tags = ["hello_#{unknown}"] + ar = PgArray.create!(tags: tags) + ar.reload + assert_equal tags, ar.tags + end + + def test_string_quoting_rules_match_pg_behavior + tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"] + x = PgArray.create!(tags: tags) + x.reload + + assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags) + end + + def test_quoting_non_standard_delimiters + strings = ["hello,", "world;"] + comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + + assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + end + + def test_mutate_array + x = PgArray.create!(tags: %w(one two)) - PgArray.update_all tags: ["four", "five"] - assert_equal ["four", "five"], pg_array.reload.tags + x.tags << "three" + x.save! + x.reload + + assert_equal %w(one two three), x.tags + assert_not x.changed? + end + + def test_mutate_value_in_array + x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }]) + + x.hstores.first['a'] = 'c' + x.save! + x.reload - PgArray.update_all tags: [] - assert_equal [], pg_array.reload.tags + assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores + assert_not x.changed? + end + + def test_datetime_with_timezone_awareness + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PgArray.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PgArray.new(datetimes: [time_string]) + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + + record.save! + record.reload + + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + end end private diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb new file mode 100644 index 0000000000..72222c01fd --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlBitStringTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlBitString < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_bit_strings', :force => true) do |t| + t.bit :a_bit, default: "00000011", limit: 8 + t.bit_varying :a_bit_varying, default: "0011", limit: 4 + end + end + + def teardown + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings' + end + + def test_bit_string_column + column = PostgresqlBitString.columns_hash["a_bit"] + assert_equal :bit, column.type + assert_equal "bit(8)", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_bit_string_varying_column + column = PostgresqlBitString.columns_hash["a_bit_varying"] + assert_equal :bit_varying, column.type + assert_equal "bit varying(4)", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit'] + assert_equal "00000011", PostgresqlBitString.new.a_bit + + assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying'] + assert_equal "0011", PostgresqlBitString.new.a_bit_varying + end + + def test_schema_dumping + output = dump_table_schema("postgresql_bit_strings") + assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output + assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output + end + + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" } + end + + def test_roundtrip + PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101" + record = PostgresqlBitString.first + assert_equal "00001010", record.a_bit + assert_equal "0101", record.a_bit_varying + + record.a_bit = "11111111" + record.a_bit_varying = "0xF" + record.save! + + assert record.reload + assert_equal "11111111", record.a_bit + assert_equal "1111", record.a_bit_varying + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index b8dd35c4c5..7872f91943 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlByteaTest < ActiveRecord::TestCase class ByteaDataType < ActiveRecord::Base @@ -19,11 +16,11 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end end end - @column = ByteaDataType.columns.find { |c| c.name == 'payload' } + @column = ByteaDataType.columns_hash['payload'] assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) end - def teardown + teardown do @connection.execute 'drop table if exists bytea_data_type' end @@ -36,16 +33,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase data = "\u001F\x8B" assert_equal('UTF-8', data.encoding.name) - assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name) + assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name) end def test_type_cast_binary_value data = "\u001F\x8B".force_encoding("BINARY") - assert_equal(data, @column.type_cast(data)) + assert_equal(data, @column.type_cast_from_database(data)) end def test_type_case_nil - assert_equal(nil, @column.type_cast(nil)) + assert_equal(nil, @column.type_cast_from_database(nil)) end def test_read_value @@ -70,6 +67,23 @@ class PostgresqlByteaTest < ActiveRecord::TestCase assert_equal(data, record.payload) end + def test_via_to_sql + data = "'\u001F\\" + ByteaDataType.create(payload: data) + sql = ByteaDataType.where(payload: data).select(:payload).to_sql + result = @connection.query(sql) + assert_equal([[data]], result) + end + + def test_via_to_sql_with_complicating_connection + Thread.new do + other_conn = ActiveRecord::Base.connection + other_conn.execute('SET standard_conforming_strings = off') + end.join + + test_via_to_sql + end + def test_write_binary data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log')) assert(data.size > 1) diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb new file mode 100644 index 0000000000..2acb64f81c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_extensions? + class PostgresqlCitextTest < ActiveRecord::TestCase + class Citext < ActiveRecord::Base + self.table_name = 'citexts' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('citext') + @connection.enable_extension 'citext' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.create_table('citexts') do |t| + t.citext 'cival' + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS citexts;' + @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;' + end + + def test_citext_enabled + assert @connection.extension_enabled?('citext') + end + + def test_column + column = Citext.columns_hash['cival'] + assert_equal :citext, column.type + assert_equal 'citext', column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_change_table_supports_json + @connection.transaction do + @connection.change_table('citexts') do |t| + t.citext 'username' + end + Citext.reset_column_information + column = Citext.columns_hash['username'] + assert_equal :citext, column.type + + raise ActiveRecord::Rollback # reset the schema change + end + ensure + Citext.reset_column_information + end + + def test_write + x = Citext.new(cival: 'Some CI Text') + x.save! + citext = Citext.first + assert_equal "Some CI Text", citext.cival + + citext.cival = "Some NEW CI Text" + citext.save! + + assert_equal "Some NEW CI Text", citext.reload.cival + end + + def test_select_case_insensitive + @connection.execute "insert into citexts (cival) values('Cased Text')" + x = Citext.where(cival: 'cased text').first + assert_equal 'Cased Text', x.cival + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 7202cce390..cfab5ca902 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/connection_helper' + +module PostgresqlCompositeBehavior + include ConnectionHelper -class PostgresqlCompositeTest < ActiveRecord::TestCase class PostgresqlComposite < ActiveRecord::Base self.table_name = "postgresql_composites" end - def teardown - @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' - @connection.execute 'DROP TYPE IF EXISTS full_address' - end - def setup + super + @connection = ActiveRecord::Base.connection @connection.transaction do @connection.execute <<-SQL @@ -29,7 +27,37 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end + def teardown + super + + @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' + @connection.execute 'DROP TYPE IF EXISTS full_address' + reset_connection + PostgresqlComposite.reset_column_information + end +end + +# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like: +# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." +# To take full advantage of composite types, we suggest you register your own +OID::Type+. +# See PostgresqlCompositeWithCustomOIDTest +class PostgresqlCompositeTest < ActiveRecord::TestCase + include PostgresqlCompositeBehavior + + def test_column + ensure_warning_is_issued + + column = PostgresqlComposite.columns_hash["address"] + assert_nil column.type + assert_equal "full_address", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + def test_composite_mapping + ensure_warning_is_issued + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" composite = PostgresqlComposite.first assert_equal "(Paris,Champs-Élysées)", composite.address @@ -39,4 +67,65 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_equal '(Paris,"Rue Basse")', composite.reload.address end + + private + def ensure_warning_is_issued + warning = capture(:stderr) do + PostgresqlComposite.columns_hash + end + assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning) + end +end + +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase + include PostgresqlCompositeBehavior + + class FullAddressType < ActiveRecord::Type::Value + def type; :full_address end + + def type_cast_from_database(value) + if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ + FullAddress.new($1, $2) + end + end + + def type_cast_from_user(value) + value + end + + def type_cast_for_database(value) + return if value.nil? + "(#{value.city},#{value.street})" + end + end + + FullAddress = Struct.new(:city, :street) + + def setup + super + + @connection.type_map.register_type "full_address", FullAddressType.new + end + + def test_column + column = PostgresqlComposite.columns_hash["address"] + assert_equal :full_address, column.type + assert_equal "full_address", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_composite_mapping + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" + composite = PostgresqlComposite.first + assert_equal "Paris", composite.address.city + assert_equal "Champs-Élysées", composite.address.street + + composite.address = FullAddress.new("Paris", "Rue Basse") + composite.save! + + assert_equal 'Paris', composite.reload.address.city + assert_equal 'Rue Basse', composite.reload.address.street + end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 4715fa002d..d26cda46fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -1,19 +1,22 @@ require "cases/helper" +require 'support/connection_helper' module ActiveRecord class PostgresqlConnectionTest < ActiveRecord::TestCase + include ConnectionHelper + class NonExistentTable < ActiveRecord::Base end def setup super @subscriber = SQLSubscriber.new - ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) + @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection end def teardown - ActiveSupport::Notifications.unsubscribe(@subscriber) + ActiveSupport::Notifications.unsubscribe(@subscription) super end @@ -45,6 +48,37 @@ module ActiveRecord assert_equal 'off', expect end + def test_reset + @connection.query('ROLLBACK') + @connection.query('SET geqo TO off') + + # Verify the setting has been applied. + expect = @connection.query('show geqo').first.first + assert_equal 'off', expect + + @connection.reset! + + # Verify the setting has been cleared. + expect = @connection.query('show geqo').first.first + assert_equal 'on', expect + end + + def test_reset_with_transaction + @connection.query('ROLLBACK') + @connection.query('SET geqo TO off') + + # Verify the setting has been applied. + expect = @connection.query('show geqo').first.first + assert_equal 'off', expect + + @connection.query('BEGIN') + @connection.reset! + + # Verify the setting has been cleared. + expect = @connection.query('show geqo').first.first + assert_equal 'on', expect + end + def test_tables_logs_name @connection.tables('hello') assert_equal 'SCHEMA', @subscriber.logged[0][1] @@ -87,7 +121,7 @@ module ActiveRecord name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})") - plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first + plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first assert_operator plan.length, :>, 0 end @@ -167,17 +201,5 @@ module ActiveRecord ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}})) end end - - private - - def run_without_connection - original_connection = ActiveRecord::Base.remove_connection - begin - yield original_connection - ensure - ActiveRecord::Base.establish_connection(original_connection) - end - end - end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 04a458fbce..a0a34e4b87 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -1,13 +1,6 @@ require "cases/helper" +require 'support/ddl_helper' -class PostgresqlArray < ActiveRecord::Base -end - -class PostgresqlTsvector < ActiveRecord::Base -end - -class PostgresqlMoney < ActiveRecord::Base -end class PostgresqlNumber < ActiveRecord::Base end @@ -15,21 +8,9 @@ end class PostgresqlTime < ActiveRecord::Base end -class PostgresqlNetworkAddress < ActiveRecord::Base -end - -class PostgresqlBitString < ActiveRecord::Base -end - class PostgresqlOid < ActiveRecord::Base end -class PostgresqlTimestampWithZone < ActiveRecord::Base -end - -class PostgresqlUUID < ActiveRecord::Base -end - class PostgresqlLtree < ActiveRecord::Base end @@ -38,57 +19,23 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.execute("set lc_monetary = 'C'") - - @connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')") - @first_array = PostgresqlArray.find(1) - - @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')") - - @first_tsvector = PostgresqlTsvector.find(1) - - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") - @first_money = PostgresqlMoney.find(1) - @second_money = PostgresqlMoney.find(2) @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") @first_number = PostgresqlNumber.find(1) + @second_number = PostgresqlNumber.find(2) + @third_number = PostgresqlNumber.find(3) @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) - @connection.execute("INSERT INTO postgresql_network_addresses (id, cidr_address, inet_address, mac_address) VALUES(1, '192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')") - @first_network_address = PostgresqlNetworkAddress.find(1) - - @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')") - @first_bit_string = PostgresqlBitString.find(1) - @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)") @first_oid = PostgresqlOid.find(1) - - @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") - - @connection.execute("INSERT INTO postgresql_uuids (id, guid, compact_guid) VALUES(1, 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')") - @first_uuid = PostgresqlUUID.find(1) - end - - def teardown - [PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress, - PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone, PostgresqlUUID].each(&:delete_all) - end - - def test_data_type_of_array_types - assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type - assert_equal :text, @first_array.column_for_attribute(:nicknames).type - end - - def test_data_type_of_tsvector_types - assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type end - def test_data_type_of_money_types - assert_equal :decimal, @first_money.column_for_attribute(:wealth).type + teardown do + [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all) end def test_data_type_of_number_types @@ -101,61 +48,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type end - def test_data_type_of_network_address_types - assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type - end - - def test_data_type_of_bit_string_types - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type - end - def test_data_type_of_oid_types assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end - def test_data_type_of_uuid_types - assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type - end - - def test_array_values - assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter - assert_equal ['foo','bar','baz'], @first_array.nicknames - end - - def test_tsvector_values - assert_equal "'text' 'vector'", @first_tsvector.text_vector - end - - def test_money_values - assert_equal 567.89, @first_money.wealth - assert_equal(-567.89, @second_money.wealth) - end - - def test_money_type_cast - column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' } - assert_equal(12345678.12, column.type_cast("$12,345,678.12")) - assert_equal(12345678.12, column.type_cast("$12.345.678,12")) - assert_equal(-1.15, column.type_cast("-$1.15")) - assert_equal(-2.25, column.type_cast("($2.25)")) - end - - def test_update_tsvector - new_text_vector = "'new' 'text' 'vector'" - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - assert_equal new_text_vector, @first_tsvector.text_vector - end - def test_number_values assert_equal 123.456, @first_number.single assert_equal 123456.789, @first_number.double + assert_equal(-::Float::INFINITY, @second_number.single) + assert_equal ::Float::INFINITY, @second_number.double + assert_same ::Float::NAN, @third_number.double end def test_time_values @@ -163,61 +65,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-21 days', @first_time.scaled_time_interval end - def test_network_address_values_ipaddr - cidr_address = IPAddr.new '192.168.0.0/24' - inet_address = IPAddr.new '172.16.1.254' - - assert_equal cidr_address, @first_network_address.cidr_address - assert_equal inet_address, @first_network_address.inet_address - assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address - end - - def test_uuid_values - assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid - assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid - end - - def test_bit_string_values - assert_equal '00010101', @first_bit_string.bit_string - assert_equal '00010101', @first_bit_string.bit_string_varying - end - def test_oid_values assert_equal 1234, @first_oid.obj_id end - def test_update_integer_array - new_value = [32800,95000,29350,17000] - @first_array.commission_by_quarter = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.commission_by_quarter - @first_array.commission_by_quarter = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.commission_by_quarter - end - - def test_update_text_array - new_value = ['robby','robert','rob','robbie'] - @first_array.nicknames = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.nicknames - @first_array.nicknames = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.nicknames - end - - def test_update_money - new_value = BigDecimal.new('123.45') - @first_money.wealth = new_value - assert @first_money.save - assert @first_money.reload - assert_equal new_value, @first_money.wealth - end - def test_update_number new_single = 789.012 new_double = 789012.345 @@ -236,51 +87,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '2 years 00:03:00', @first_time.time_interval end - def test_update_network_address - new_inet_address = '10.1.2.3/32' - new_cidr_address = '10.0.0.0/8' - new_mac_address = 'bc:de:f0:12:34:56' - @first_network_address.cidr_address = new_cidr_address - @first_network_address.inet_address = new_inet_address - @first_network_address.mac_address = new_mac_address - assert @first_network_address.save - assert @first_network_address.reload - assert_equal @first_network_address.cidr_address, new_cidr_address - assert_equal @first_network_address.inet_address, new_inet_address - assert_equal @first_network_address.mac_address, new_mac_address - end - - def test_update_bit_string - new_bit_string = '11111111' - new_bit_string_varying = '0xFF' - @first_bit_string.bit_string = new_bit_string - @first_bit_string.bit_string_varying = new_bit_string_varying - assert @first_bit_string.save - assert @first_bit_string.reload - assert_equal new_bit_string, @first_bit_string.bit_string - assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying - end - - def test_invalid_hex_string - new_bit_string = 'FF' - @first_bit_string.bit_string = new_bit_string - assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } - end - - def test_invalid_network_address - @first_network_address.cidr_address = 'invalid addr' - assert_nil @first_network_address.cidr_address - assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast - assert @first_network_address.save - - @first_network_address.reload - - @first_network_address.inet_address = 'invalid addr' - assert_nil @first_network_address.inet_address - assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast - assert @first_network_address.save - end - def test_update_oid new_value = 567890 @first_oid.obj_id = new_value @@ -288,30 +94,26 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert @first_oid.reload assert_equal new_value, @first_oid.obj_id end +end - def test_timestamp_with_zone_values_with_rails_time_zone_support - with_timezone_config default: :utc, aware_attributes: true do - @connection.reconnect! +class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase + include DdlHelper - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time - end - ensure - @connection.reconnect! + setup do + @connection = ActiveRecord::Base.connection end - def test_timestamp_with_zone_values_without_rails_time_zone_support - with_timezone_config default: :local, aware_attributes: false do - @connection.reconnect! - # make sure to use a non-UTC time zone - @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + def test_name_column_type + with_example_table @connection, 'ex', 'data name' do + column = @connection.columns('ex').find { |col| col.name == 'data' } + assert_equal :string, column.type + end + end - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + def test_char_column_type + with_example_table @connection, 'ex', 'data "char"' do + column = @connection.columns('ex').find { |col| col.name == 'data' } + assert_equal :string, column.type end - ensure - @connection.reconnect! end end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb new file mode 100644 index 0000000000..1500adb42d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' + +class PostgresqlDomainTest < ActiveRecord::TestCase + include ConnectionHelper + + class PostgresqlDomain < ActiveRecord::Base + self.table_name = "postgresql_domains" + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute "CREATE DOMAIN custom_money as numeric(8,2)" + @connection.create_table('postgresql_domains') do |t| + t.column :price, :custom_money + end + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_domains' + @connection.execute 'DROP DOMAIN IF EXISTS custom_money' + reset_connection + end + + def test_column + column = PostgresqlDomain.columns_hash["price"] + assert_equal :decimal, column.type + assert_equal "custom_money", column.sql_type + assert column.number? + assert_not column.binary? + assert_not column.array + end + + def test_domain_acts_like_basetype + PostgresqlDomain.create price: "" + record = PostgresqlDomain.first + assert_nil record.price + + record.price = "34.15" + record.save! + + assert_equal BigDecimal.new("34.15"), record.reload.price + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb new file mode 100644 index 0000000000..d99c4a292e --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlEnumTest < ActiveRecord::TestCase + include ConnectionHelper + + class PostgresqlEnum < ActiveRecord::Base + self.table_name = "postgresql_enums" + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute <<-SQL + CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); + SQL + @connection.create_table('postgresql_enums') do |t| + t.column :current_mood, :mood + end + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_enums' + @connection.execute 'DROP TYPE IF EXISTS mood' + reset_connection + end + + def test_column + column = PostgresqlEnum.columns_hash["current_mood"] + assert_equal :enum, column.type + assert_equal "mood", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_enum_defaults + @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy' + PostgresqlEnum.reset_column_information + + assert_equal "happy", PostgresqlEnum.column_defaults['good_mood'] + assert_equal "happy", PostgresqlEnum.new.good_mood + ensure + PostgresqlEnum.reset_column_information + end + + def test_enum_mapping + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + enum = PostgresqlEnum.first + assert_equal "sad", enum.current_mood + + enum.current_mood = "happy" + enum.save! + + assert_equal "happy", enum.reload.current_mood + end + + def test_invalid_enum_update + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + enum = PostgresqlEnum.first + enum.current_mood = "angry" + + assert_raise ActiveRecord::StatementInvalid do + enum.save + end + end + + def test_no_oid_warning + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + stderr_output = capture(:stderr) { PostgresqlEnum.first } + + assert stderr_output.blank? + end + + def test_enum_type_cast + enum = PostgresqlEnum.new + enum.current_mood = :happy + + assert_equal "happy", enum.current_mood + end +end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 0b61f61572..416f84cb38 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -9,7 +9,7 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain assert_match %(QUERY PLAN), explain assert_match %(Index Scan using developers_pkey on developers), explain end @@ -17,9 +17,9 @@ module ActiveRecord def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain assert_match %(Index Scan using developers_pkey on developers), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(Seq Scan on audit_logs), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb new file mode 100644 index 0000000000..7b99fcdda0 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -0,0 +1,63 @@ +require "cases/helper" + +class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class EnableHstore < ActiveRecord::Migration + def change + enable_extension "hstore" + end + end + + class DisableHstore < ActiveRecord::Migration + def change + disable_extension "hstore" + end + end + + def setup + super + + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip("no extension support") + end + + @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name + @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix + @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix + + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s" + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix + ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = true + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name + + super + end + + def test_enable_extension_migration_ignores_prefix_and_suffix + @connection.disable_extension("hstore") + + migrations = [EnableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled" + end + + def test_disable_extension_migration_ignores_prefix_and_suffix + @connection.enable_extension("hstore") + + migrations = [DisableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled" + end +end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb new file mode 100644 index 0000000000..9dadb177ca --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -0,0 +1,26 @@ +# encoding: utf-8 +require "cases/helper" + +class PostgresqlFullTextTest < ActiveRecord::TestCase + class PostgresqlTsvector < ActiveRecord::Base; end + + def test_tsvector_column + column = PostgresqlTsvector.columns_hash["text_vector"] + assert_equal :tsvector, column.type + assert_equal "tsvector", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_update_tsvector + PostgresqlTsvector.create text_vector: "'text' 'vector'" + tsvector = PostgresqlTsvector.first + assert_equal "'text' 'vector'", tsvector.text_vector + + tsvector.text_vector = "'new' 'text' 'vector'" + tsvector.save! + assert tsvector.reload + assert_equal "'new' 'text' 'vector'", tsvector.text_vector + end +end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb new file mode 100644 index 0000000000..6c0adbbeaa --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlPointTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlPoint < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.create_table('postgresql_points') do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" + end + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_points' + end + + def test_column + column = PostgresqlPoint.columns_hash["x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] + assert_equal [12.2, 13.3], PostgresqlPoint.new.y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] + assert_equal [14.4, 15.5], PostgresqlPoint.new.z + end + + def test_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"x"$}, output + assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_roundtrip + PostgresqlPoint.create! x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.x + + record.x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.x + end + + def test_mutation + p = PostgresqlPoint.create! x: [10, 20] + + p.x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.x + assert_not p.changed? + end +end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index d8782f5eaa..1296eb72c0 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -24,13 +24,14 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('hstores') do |t| t.hstore 'tags', :default => '' + t.hstore 'payload', array: true t.hstore 'settings' end end - @column = Hstore.columns.find { |c| c.name == 'tags' } + @column = Hstore.columns_hash['tags'] end - def teardown + teardown do @connection.execute 'drop table if exists hstores' end @@ -53,6 +54,20 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_column assert_equal :hstore, @column.type + assert_equal "hstore", @column.sql_type + assert_not @column.number? + assert_not @column.binary? + assert_not @column.array + end + + def test_default + @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"' + Hstore.reset_column_information + + assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions']) + assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions) + ensure + Hstore.reset_column_information end def test_change_table_supports_hstore @@ -61,7 +76,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase t.hstore 'users', default: '' end Hstore.reset_column_information - column = Hstore.columns.find { |c| c.name == 'users' } + column = Hstore.columns_hash['users'] assert_equal :hstore, column.type raise ActiveRecord::Rollback # reset the schema change @@ -89,22 +104,17 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_cast_value_on_write x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast) assert_equal({"bool" => "true", "number" => "5"}, x.tags) x.save assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) end def test_type_cast_hstore - assert @column - - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast(data)) - - assert_equal({}, @column.type_cast("")) - assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) + assert_equal({}, @column.type_cast_from_database("")) + assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -125,48 +135,78 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal "GMT", x.timezone end + def test_duplication_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = x.dup + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_yaml_round_trip_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = YAML.load(YAML.dump(x)) + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_changes_in_place + hstore = Hstore.create!(settings: { 'one' => 'two' }) + hstore.settings['three'] = 'four' + hstore.save! + hstore.reload + + assert_equal 'four', hstore.settings['three'] + assert_not hstore.changed? + end + def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) end def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast("==>>")) + assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w')) end def test_rewrite @@ -182,6 +222,30 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal({'1' => '2'}, x.tags) end + def test_array_cycle + assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}]) + end + + def test_array_strings_with_quotes + assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}]) + end + + def test_array_strings_with_commas + assert_array_cycle([{'this,has' => 'many,values'}]) + end + + def test_array_strings_with_array_delimiters + assert_array_cycle(['{' => '}']) + end + + def test_array_strings_with_null_strings + assert_array_cycle([{'NULL' => 'NULL'}]) + end + + def test_contains_nils + assert_array_cycle([{'NULL' => nil}]) + end + def test_select_multikey @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" x = Hstore.first @@ -224,19 +288,51 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle("a\nb" => "c\nd") end - def test_update_all - hstore = Hstore.create! tags: { "one" => "two" } + class TagCollection + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end - Hstore.update_all tags: { "three" => "four" } - assert_equal({ "three" => "four" }, hstore.reload.tags) + class HstoreWithSerialize < Hstore + serialize :tags, TagCollection + end + + def test_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + assert_instance_of TagCollection, record.tags + assert_equal({"one" => "two"}, record.tags.to_hash) + record.tags = TagCollection.new("three" => "four") + record.save! + assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash) + end - Hstore.update_all tags: { } - assert_equal({ }, hstore.reload.tags) + def test_clone_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + dupe = record.dup + assert_equal({"one" => "two"}, dupe.tags.to_hash) end end private + def assert_array_cycle(array) + # test creation + x = Hstore.create!(payload: array) + x.reload + assert_equal(array, x.payload) + + # test updating + x = Hstore.create!(payload: []) + x.payload = array + x.save! + x.reload + assert_equal(array, x.payload) + end + def assert_cycle(hash) # test creation x = Hstore.create!(:tags => hash) diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..22e8873333 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,44 @@ +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::TestCase + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 3daef399d8..86ba849445 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -4,7 +4,7 @@ require "cases/helper" require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' -class PostgresqlJSONTest < ActiveRecord::TestCase +module PostgresqlJSONSharedTestCases class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' @@ -16,14 +16,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('json_data_type') do |t| - t.json 'payload', :default => {} - t.json 'settings' + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end end rescue ActiveRecord::StatementInvalid skip "do not test on PG without json" end - @column = JsonDataType.columns.find { |c| c.name == 'payload' } + @column = JsonDataType.columns_hash['payload'] end def teardown @@ -31,17 +31,32 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end def test_column - assert_equal :json, @column.type + column = JsonDataType.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' + JsonDataType.reset_column_information + + assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) + assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions) + ensure + JsonDataType.reset_column_information end def test_change_table_supports_json @connection.transaction do @connection.change_table('json_data_type') do |t| - t.json 'users', default: '{}' + t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}' end JsonDataType.reset_column_information - column = JsonDataType.columns.find { |c| c.name == 'users' } - assert_equal :json, column.type + column = JsonDataType.columns_hash['users'] + assert_equal column_type, column.type raise ActiveRecord::Rollback # reset the schema change end @@ -51,22 +66,23 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_cast_value_on_write x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) x.save assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) end def test_type_cast_json - assert @column + column = JsonDataType.columns_hash["payload"] data = "{\"a_key\":\"a_value\"}" - hash = @column.class.string_to_json data + hash = column.type_cast_from_database(data) assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, @column.type_cast(data)) + assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) - assert_equal({}, @column.type_cast("{}")) - assert_equal({'key'=>nil}, @column.type_cast('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"}))) + assert_equal({}, column.type_cast_from_database("{}")) + assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"}))) end def test_rewrite @@ -122,13 +138,56 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "640×1136", x.resolution end - def test_update_all - json = JsonDataType.create! payload: { "one" => "two" } + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end +end + +class PostgresqlJSONTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :json + end +end - JsonDataType.update_all payload: { "three" => "four" } - assert_equal({ "three" => "four" }, json.reload.payload) +class PostgresqlJSONBTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases - JsonDataType.update_all payload: { } - assert_equal({ }, json.reload.payload) + def column_type + :jsonb end end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 5d12ca75ca..889e369bd6 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,5 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlLtreeTest < ActiveRecord::TestCase class Ltree < ActiveRecord::Base @@ -10,6 +8,11 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('ltree') + @connection.enable_extension 'ltree' + end + @connection.transaction do @connection.create_table('ltrees') do |t| t.ltree 'path' @@ -19,13 +22,17 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase skip "do not test on PG without ltree" end - def teardown + teardown do @connection.execute 'drop table if exists ltrees' end def test_column column = Ltree.columns_hash['path'] assert_equal :ltree, column.type + assert_equal "ltree", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array end def test_write diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb new file mode 100644 index 0000000000..87183174f2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -0,0 +1,96 @@ +# encoding: utf-8 +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlMoneyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + class PostgresqlMoney < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("set lc_monetary = 'C'") + @connection.create_table('postgresql_moneys') do |t| + t.column "wealth", "money" + t.column "depth", "money", default: "150.55" + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys' + end + + def test_column + column = PostgresqlMoney.columns_hash["wealth"] + assert_equal :money, column.type + assert_equal "money", column.sql_type + assert_equal 2, column.scale + assert column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth'] + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth + end + + def test_money_values + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") + + first_money = PostgresqlMoney.find(1) + second_money = PostgresqlMoney.find(2) + assert_equal 567.89, first_money.wealth + assert_equal(-567.89, second_money.wealth) + end + + def test_money_type_cast + column = PostgresqlMoney.columns_hash['wealth'] + assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12")) + assert_equal(-1.15, column.type_cast_from_user("-$1.15")) + assert_equal(-2.25, column.type_cast_from_user("($2.25)")) + end + + def test_schema_dumping + output = dump_table_schema("postgresql_moneys") + assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output + end + + def test_create_and_update_money + money = PostgresqlMoney.create(wealth: "987.65") + assert_equal 987.65, money.wealth + + new_value = BigDecimal.new('123.45') + money.wealth = new_value + money.save! + money.reload + assert_equal new_value, money.wealth + end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: '123.45'.to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end +end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb new file mode 100644 index 0000000000..4f4c1103fa --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 +require "cases/helper" + +class PostgresqlNetworkTest < ActiveRecord::TestCase + class PostgresqlNetworkAddress < ActiveRecord::Base + end + + def test_cidr_column + column = PostgresqlNetworkAddress.columns_hash["cidr_address"] + assert_equal :cidr, column.type + assert_equal "cidr", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_inet_column + column = PostgresqlNetworkAddress.columns_hash["inet_address"] + assert_equal :inet, column.type + assert_equal "inet", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_macaddr_column + column = PostgresqlNetworkAddress.columns_hash["mac_address"] + assert_equal :macaddr, column.type + assert_equal "macaddr", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_network_types + PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24', + inet_address: '172.16.1.254/32', + mac_address: '01:23:45:67:89:0a') + + address = PostgresqlNetworkAddress.first + assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address + assert_equal IPAddr.new('172.16.1.254'), address.inet_address + assert_equal '01:23:45:67:89:0a', address.mac_address + + address.cidr_address = '10.1.2.3/32' + address.inet_address = '10.0.0.0/8' + address.mac_address = 'bc:de:f0:12:34:56' + + address.save! + assert address.reload + assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address + assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address + assert_equal 'bc:de:f0:12:34:56', address.mac_address + end + + def test_invalid_network_address + invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr', + inet_address: 'invalid addr') + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast + assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast + assert invalid_address.save + + invalid_address.reload + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_nil invalid_address.cidr_address_before_type_cast + assert_nil invalid_address.inet_address_before_type_cast + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 019406dd84..cfff1f980b 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,9 +1,14 @@ # encoding: utf-8 require "cases/helper" +require 'support/ddl_helper' +require 'support/connection_helper' module ActiveRecord module ConnectionAdapters class PostgreSQLAdapterTest < ActiveRecord::TestCase + include DdlHelper + include ConnectionHelper + def setup @connection = ActiveRecord::Base.connection end @@ -129,18 +134,18 @@ module ActiveRecord end def test_default_sequence_name - assert_equal 'accounts_id_seq', + assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), @connection.default_sequence_name('accounts', 'id') - assert_equal 'accounts_id_seq', + assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), @connection.default_sequence_name('accounts') end def test_default_sequence_name_bad_table - assert_equal 'zomg_id_seq', + assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), @connection.default_sequence_name('zomg', 'id') - assert_equal 'zomg_id_seq', + assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), @connection.default_sequence_name('zomg') end @@ -176,6 +181,51 @@ module ActiveRecord assert_nil @connection.pk_and_sequence_for('unobtainium') end + def test_pk_and_sequence_for_with_collision_pg_class_oid + @connection.exec_query('create table ex(id serial primary key)') + @connection.exec_query('create table ex2(id serial primary key)') + + correct_depend_record = [ + "'pg_class'::regclass", + "'ex_id_seq'::regclass", + '0', + "'pg_class'::regclass", + "'ex'::regclass", + '1', + "'a'" + ] + + collision_depend_record = [ + "'pg_attrdef'::regclass", + "'ex2_id_seq'::regclass", + '0', + "'pg_class'::regclass", + "'ex'::regclass", + '1', + "'a'" + ] + + @connection.exec_query( + "DELETE FROM pg_depend WHERE objid = 'ex_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" + ) + @connection.exec_query( + "INSERT INTO pg_depend VALUES(#{collision_depend_record.join(',')})" + ) + @connection.exec_query( + "INSERT INTO pg_depend VALUES(#{correct_depend_record.join(',')})" + ) + + seq = @connection.pk_and_sequence_for('ex').last + assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq + + @connection.exec_query( + "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" + ) + ensure + @connection.exec_query('DROP TABLE IF EXISTS ex') + @connection.exec_query('DROP TABLE IF EXISTS ex2') + end + def test_exec_insert_number with_example_table do insert(@connection, 'number' => 10) @@ -303,12 +353,77 @@ module ActiveRecord assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) end + def test_columns_for_distinct_without_order_specifiers + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"]) + end + def test_raise_error_when_cannot_translate_exception assert_raise TypeError do @connection.send(:log, nil) { @connection.execute(nil) } end end + def test_reload_type_map_for_newly_defined_types + @connection.execute "CREATE TYPE feeling AS ENUM ('good', 'bad')" + result = @connection.select_all "SELECT 'good'::feeling" + assert_instance_of(PostgreSQLAdapter::OID::Enum, + result.column_types["feeling"]) + ensure + @connection.execute "DROP TYPE IF EXISTS feeling" + reset_connection + end + + def test_only_reload_type_map_once_for_every_unknown_type + silence_warnings do + assert_queries 2, ignore_none: true do + @connection.select_all "SELECT NULL::anyelement" + end + assert_queries 1, ignore_none: true do + @connection.select_all "SELECT NULL::anyelement" + end + assert_queries 2, ignore_none: true do + @connection.select_all "SELECT NULL::anyarray" + end + end + ensure + reset_connection + end + + def test_only_warn_on_first_encounter_of_unknown_oid + warning = capture(:stderr) { + @connection.select_all "SELECT NULL::anyelement" + @connection.select_all "SELECT NULL::anyelement" + @connection.select_all "SELECT NULL::anyelement" + } + assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning) + ensure + reset_connection + end + + def test_unparsed_defaults_are_at_least_set_when_saving + with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do + number_klass = Class.new(ActiveRecord::Base) do + self.table_name = 'ex' + end + column = number_klass.columns_hash["number"] + assert_nil column.default + assert_nil column.default_function + + first_number = number_klass.new + assert_nil first_number.number + + first_number.save! + assert_equal 4, first_number.reload.number + end + end + private def insert(ctx, data) binds = data.map { |name, value| @@ -324,12 +439,8 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end - def with_example_table(definition = nil) - definition ||= 'id serial primary key, number integer, data character varying(255)' - @connection.exec_query("create table ex(#{definition})") - yield - ensure - @connection.exec_query('drop table if exists ex') + def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block) + super(@connection, 'ex', definition, &block) end def connection_without_insert_returning diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 1122f8b9a1..11d5173d37 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,46 +10,46 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 't', @conn.type_cast(true, nil) assert_equal 't', @conn.type_cast(true, c) end def test_type_cast_false - c = Column.new(nil, 1, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end def test_type_cast_cidr ip = IPAddr.new('255.0.0.0/8') - c = Column.new(nil, ip, 'cidr') + c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr') assert_equal ip, @conn.type_cast(ip, c) end def test_type_cast_inet ip = IPAddr.new('255.1.0.0/8') - c = Column.new(nil, ip, 'inet') + c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet') assert_equal ip, @conn.type_cast(ip, c) end def test_quote_float_nan nan = 0.0/0 - c = Column.new(nil, 1, 'float') + c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') assert_equal "'NaN'", @conn.quote(nan, c) end def test_quote_float_infinity infinity = 1.0/0 - c = Column.new(nil, 1, 'float') + c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') assert_equal "'Infinity'", @conn.quote(infinity, c) end def test_quote_cast_numeric fixnum = 666 - c = Column.new(nil, nil, 'varchar') + c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') assert_equal "'666'", @conn.quote(fixnum, c) - c = Column.new(nil, nil, 'text') + c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end @@ -57,6 +57,17 @@ module ActiveRecord assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) + assert_equal "'[1,0]'", @conn.quote(range, c) + end + + def test_quote_bit_string + c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) + assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 5c2d8e1c1d..d812cd01c4 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,6 +1,5 @@ require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/connection_helper' if ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base @@ -8,14 +7,20 @@ if ActiveRecord::Base.connection.supports_ranges? end class PostgresqlRangeTest < ActiveRecord::TestCase - def teardown - @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' - end + self.use_transactional_fixtures = false + include ConnectionHelper def setup - @connection = ActiveRecord::Base.connection + @connection = PostgresqlRange.connection begin @connection.transaction do + @connection.execute <<_SQL + CREATE TYPE floatrange AS RANGE ( + subtype = float8, + subtype_diff = float8mi + ); +_SQL + @connection.create_table('postgresql_ranges') do |t| t.daterange :date_range t.numrange :num_range @@ -24,7 +29,10 @@ if ActiveRecord::Base.connection.supports_ranges? t.int4range :int4_range t.int8range :int8_range end + + @connection.add_column 'postgresql_ranges', 'float_range', 'floatrange' end + PostgresqlRange.reset_column_information rescue ActiveRecord::StatementInvalid skip "do not test on PG without range" end @@ -35,23 +43,26 @@ if ActiveRecord::Base.connection.supports_ranges? ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", int4_range: "[1, 10]", - int8_range: "[10, 100]") + int8_range: "[10, 100]", + float_range: "[0.5, 0.7]") insert_range(id: 102, - date_range: "(''2012-01-02'', ''2012-01-04'')", + date_range: "[''2012-01-02'', ''2012-01-04'')", num_range: "[0.1, 0.2)", ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", - int4_range: "(1, 10)", - int8_range: "(10, 100)") + int4_range: "[1, 10)", + int8_range: "[10, 100)", + float_range: "[0.5, 0.7)") insert_range(id: 103, - date_range: "(''2012-01-02'',]", + date_range: "[''2012-01-02'',]", num_range: "[0.1,]", ts_range: "[''2010-01-01 14:30'',]", tstz_range: "[''2010-01-01 14:30:00+05'',]", - int4_range: "(1,]", - int8_range: "(10,]") + int4_range: "[1,]", + int8_range: "[10,]", + float_range: "[0.5,]") insert_range(id: 104, date_range: "[,]", @@ -59,15 +70,17 @@ if ActiveRecord::Base.connection.supports_ranges? ts_range: "[,]", tstz_range: "[,]", int4_range: "[,]", - int8_range: "[,]") + int8_range: "[,]", + float_range: "[,]") insert_range(id: 105, - date_range: "(''2012-01-02'', ''2012-01-02'')", - num_range: "(0.1, 0.1)", - ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')", - tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", - int4_range: "(1, 1)", - int8_range: "(10, 10)") + date_range: "[''2012-01-02'', ''2012-01-02'')", + num_range: "[0.1, 0.1)", + ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "[1, 1)", + int8_range: "[10, 10)", + float_range: "[0.5, 0.5)") @new_range = PostgresqlRange.new @first_range = PostgresqlRange.find(101) @@ -77,6 +90,12 @@ if ActiveRecord::Base.connection.supports_ranges? @empty_range = PostgresqlRange.find(105) end + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' + @connection.execute 'DROP TYPE IF EXISTS floatrange' + reset_connection + end + def test_data_type_of_range_types assert_equal :daterange, @first_range.column_for_attribute(:date_range).type assert_equal :numrange, @first_range.column_for_attribute(:num_range).type @@ -88,24 +107,24 @@ if ActiveRecord::Base.connection.supports_ranges? def test_int4range_values assert_equal 1...11, @first_range.int4_range - assert_equal 2...10, @second_range.int4_range - assert_equal 2...Float::INFINITY, @third_range.int4_range + assert_equal 1...10, @second_range.int4_range + assert_equal 1...Float::INFINITY, @third_range.int4_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) assert_nil @empty_range.int4_range end def test_int8range_values assert_equal 10...101, @first_range.int8_range - assert_equal 11...100, @second_range.int8_range - assert_equal 11...Float::INFINITY, @third_range.int8_range + assert_equal 10...100, @second_range.int8_range + assert_equal 10...Float::INFINITY, @third_range.int8_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) assert_nil @empty_range.int8_range end def test_daterange_values assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) assert_nil @empty_range.date_range end @@ -133,6 +152,14 @@ if ActiveRecord::Base.connection.supports_ranges? assert_nil @empty_range.tstz_range end + def test_custom_range_values + assert_equal 0.5..0.7, @first_range.float_range + assert_equal 0.5...0.7, @second_range.float_range + assert_equal 0.5...Float::INFINITY, @third_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) + assert_nil @empty_range.float_range + end + def test_create_tstzrange tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') round_trip(@new_range, :tstz_range, tstzrange) @@ -203,6 +230,55 @@ if ActiveRecord::Base.connection.supports_ranges? assert_nil_round_trip(@first_range, :int8_range, 39999...39999) end + def test_exclude_beginning_for_subtypes_with_succ_method_is_deprecated + tz = ::ActiveRecord::Base.default_timezone + + silence_warnings { + assert_deprecated { + range = PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") + assert_equal Date.new(2012, 1, 3)..Date.new(2012, 1, 4), range.date_range + } + assert_deprecated { + range = PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 1)..Time.send(tz, 2011, 1, 1, 14, 30, 0), range.ts_range + } + assert_deprecated { + range = PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") + assert_equal Time.parse('2010-01-01 09:30:01 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), range.tstz_range + } + assert_deprecated { + range = PostgresqlRange.create!(int4_range: "(1, 10]") + assert_equal 2..10, range.int4_range + } + assert_deprecated { + range = PostgresqlRange.create!(int8_range: "(10, 100]") + assert_equal 11..100, range.int8_range + } + } + end + + def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported + assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + end + + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) @@ -229,7 +305,8 @@ if ActiveRecord::Base.connection.supports_ranges? ts_range, tstz_range, int4_range, - int8_range + int8_range, + float_range ) VALUES ( #{values[:id]}, '#{values[:date_range]}', @@ -237,7 +314,8 @@ if ActiveRecord::Base.connection.supports_ranges? '#{values[:ts_range]}', '#{values[:tstz_range]}', '#{values[:int4_range]}', - '#{values[:int8_range]}' + '#{values[:int8_range]}', + '#{values[:float_range]}' ) SQL end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index d5e1838543..99c26c4bf7 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -27,7 +27,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase end end - def teardown + teardown do set_session_auth @connection.execute "RESET search_path" USERS.each do |u| diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 3f7009c1d1..9e5fd17dc4 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -50,6 +50,16 @@ class SchemaTest < ActiveRecord::TestCase self.table_name = 'things' end + class Song < ActiveRecord::Base + self.table_name = "music.songs" + has_and_belongs_to_many :albums + end + + class Album < ActiveRecord::Base + self.table_name = "music.albums" + has_and_belongs_to_many :songs + end + def setup @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @@ -71,7 +81,7 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" end - def teardown + teardown do @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE" @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" end @@ -109,6 +119,22 @@ class SchemaTest < ActiveRecord::TestCase assert !@connection.schema_names.include?("test_schema3") end + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.execute <<-SQL + DROP SCHEMA IF EXISTS music CASCADE; + CREATE SCHEMA music; + CREATE TABLE music.albums (id serial primary key); + CREATE TABLE music.songs (id serial primary key); + CREATE TABLE music.albums_songs (album_id integer, song_id integer); + SQL + + song = Song.create + Album.create + assert_equal song, Song.includes(:albums).references(:albums).first + ensure + ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" + end + def test_raise_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do @connection.drop_schema "test_schema3" @@ -271,13 +297,13 @@ class SchemaTest < ActiveRecord::TestCase end def test_with_uppercase_index_name - ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"} + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"} + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - ActiveRecord::Base.connection.schema_search_path = SCHEMA_NAME - assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "things_Index"} - ActiveRecord::Base.connection.schema_search_path = "public" + with_schema_search_path SCHEMA_NAME do + assert_nothing_raised { @connection.remove_index! "things", "things_Index"} + end end def test_primary_key_with_schema_specified @@ -305,14 +331,15 @@ class SchemaTest < ActiveRecord::TestCase end def test_pk_and_sequence_for_with_schema_specified + pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") ].each do |given| pk, seq = @connection.pk_and_sequence_for(given) assert_equal 'id', pk, "primary key should be found when table referenced as #{given}" - assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") - assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end @@ -328,18 +355,17 @@ class SchemaTest < ActiveRecord::TestCase end def test_prepared_statements_with_multiple_schemas + [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| + with_schema_search_path schema_name do + Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now) + end + end - @connection.schema_search_path = SCHEMA_NAME - Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now) - - @connection.schema_search_path = SCHEMA2_NAME - Thing5.create(:id => 1, :name => "thing inside #{SCHEMA2_NAME}", :email => "thing1@localhost", :moment => Time.now) - - @connection.schema_search_path = SCHEMA_NAME - assert_equal 1, Thing5.count - - @connection.schema_search_path = SCHEMA2_NAME - assert_equal 1, Thing5.count + [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| + with_schema_search_path schema_name do + assert_equal 1, Thing5.count + end + end end def test_schema_exists? @@ -353,6 +379,14 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_reset_pk_sequence + sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" + @connection.execute "SELECT setval('#{sequence_name}', 123)" + assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") + assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + end + private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 4d29a20e66..3614b29190 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,6 +2,47 @@ require 'cases/helper' require 'models/developer' require 'models/topic' +class PostgresqlTimestampTest < ActiveRecord::TestCase + class PostgresqlTimestampWithZone < ActiveRecord::Base; end + + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") + end + + teardown do + PostgresqlTimestampWithZone.delete_all + end + + def test_timestamp_with_zone_values_with_rails_time_zone_support + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end + + def test_timestamp_with_zone_values_without_rails_time_zone_support + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end +end + class TimestampTest < ActiveRecord::TestCase fixtures :topics @@ -82,20 +123,32 @@ class TimestampTest < ActiveRecord::TestCase assert_equal date, Developer.find_by_name("aaron").updated_at end + def test_bc_timestamp_leap_year + date = Time.utc(-4, 2, 29) + Developer.create!(:name => "taihou", :updated_at => date) + assert_equal date, Developer.find_by_name("taihou").updated_at + end + + def test_bc_timestamp_year_zero + date = Time.utc(0, 4, 7) + Developer.create!(:name => "yahagi", :updated_at => date) + assert_equal date, Developer.find_by_name("yahagi").updated_at + end + private - def pg_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] + def pg_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name end + result && result["datetime_precision"] + end - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name end + result && result.send(option) + end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb new file mode 100644 index 0000000000..23817198b1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -0,0 +1,15 @@ +require 'cases/helper' + +class PostgresqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "array delimiters are looked up correctly" do + box_array = @connection.type_map.lookup(1020) + int_array = @connection.type_map.lookup(1007) + + assert_equal ';', box_array.delimiter + assert_equal ',', int_array.delimiter + end +end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 9e7b08ef34..3fdb6888d9 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,9 +1,10 @@ require 'cases/helper' class PostgreSQLUtilsTest < ActiveSupport::TestCase - include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils - def test_extract_schema_and_table + def test_extract_schema_qualified_name { %(table_name) => [nil,'table_name'], %("table.name") => [nil,'table.name'], @@ -14,7 +15,47 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase %("even spaces".table) => ['even spaces','table'], %(schema."table.name") => ['schema', 'table.name'] }.each do |given, expect| - assert_equal expect, extract_schema_and_table(given) + assert_equal Name.new(*expect), extract_schema_qualified_name(given) end end end + +class PostgreSQLNameTest < ActiveSupport::TestCase + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + + test "represents itself as schema.name" do + obj = Name.new("public", "articles") + assert_equal "public.articles", obj.to_s + end + + test "without schema, represents itself as name only" do + obj = Name.new(nil, "articles") + assert_equal "articles", obj.to_s + end + + test "quoted returns a string representation usable in a query" do + assert_equal %("articles"), Name.new(nil, "articles").quoted + assert_equal %("public"."articles"), Name.new("public", "articles").quoted + end + + test "prevents double quoting" do + name = Name.new('"quoted_schema"', '"quoted_table"') + assert_equal "quoted_schema.quoted_table", name.to_s + assert_equal %("quoted_schema"."quoted_table"), name.quoted + end + + test "equality based on state" do + assert_equal Name.new("access", "users"), Name.new("access", "users") + assert_equal Name.new(nil, "users"), Name.new(nil, "users") + assert_not_equal Name.new(nil, "users"), Name.new("access", "users") + assert_not_equal Name.new("access", "users"), Name.new("public", "users") + assert_not_equal Name.new("public", "users"), Name.new("public", "articles") + end + + test "can be used as hash key" do + hash = {Name.new("schema", "article_seq") => "success"} + assert_equal "success", hash[Name.new("schema", "article_seq")] + assert_equal nil, hash[Name.new("schema", "articles")] + assert_equal nil, hash[Name.new("public", "article_seq")] + end +end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 3f5d981444..66006d718f 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -4,31 +4,146 @@ require "cases/helper" require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' +module PostgresqlUUIDHelper + def connection + @connection ||= ActiveRecord::Base.connection + end + + def drop_table(name) + connection.execute "drop table if exists #{name}" + end +end + class PostgresqlUUIDTest < ActiveRecord::TestCase + include PostgresqlUUIDHelper + + class UUIDType < ActiveRecord::Base + self.table_name = "uuid_data_type" + end + + setup do + connection.create_table "uuid_data_type" do |t| + t.uuid 'guid' + end + end + + teardown do + drop_table "uuid_data_type" + end + + def test_change_column_default + @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" + UUIDType.reset_column_information + column = UUIDType.columns_hash['thingy'] + assert_equal "uuid_generate_v1()", column.default_function + + @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" + + UUIDType.reset_column_information + column = UUIDType.columns_hash['thingy'] + assert_equal "uuid_generate_v4()", column.default_function + ensure + UUIDType.reset_column_information + end + + def test_data_type_of_uuid_types + column = UUIDType.columns_hash["guid"] + assert_equal :uuid, column.type + assert_equal "uuid", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_treat_blank_uuid_as_nil + UUIDType.create! guid: '' + assert_equal(nil, UUIDType.last.guid) + end + + def test_treat_invalid_uuid_as_nil + uuid = UUIDType.create! guid: 'foobar' + assert_equal(nil, uuid.guid) + end + + def test_invalid_uuid_dont_modify_before_type_cast + uuid = UUIDType.new guid: 'foobar' + assert_equal 'foobar', uuid.guid_before_type_cast + end + + def test_rfc_4122_regex + # Valid uuids + ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', + 'a0eebc999c0b4ef8bb6d6bb9bd380a11', + 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid| + uuid = UUIDType.new guid: valid_uuid + assert_not_nil uuid.guid + end + + # Invalid uuids + [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'], + Hash.new, + 0, + 0.0, + true, + 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + 'a0eebc999r0b4ef8ab6d6bb9bd380a11', + 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| + uuid = UUIDType.new guid: invalid_uuid + assert_nil uuid.guid + end + end + + def test_uuid_formats + ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", + "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", + "a0eebc999c0b4ef8bb6d6bb9bd380a11", + "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |valid_uuid| + UUIDType.create(guid: valid_uuid) + uuid = UUIDType.last + assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid + end + end +end + +class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase + include PostgresqlUUIDHelper + class UUID < ActiveRecord::Base self.table_name = 'pg_uuids' end - def setup - @connection = ActiveRecord::Base.connection + setup do + enable_uuid_ossp!(connection) - unless @connection.extension_enabled?('uuid-ossp') - @connection.enable_extension 'uuid-ossp' - @connection.commit_db_transaction + connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| + t.string 'name' + t.uuid 'other_uuid', default: 'uuid_generate_v4()' end - @connection.reconnect! + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM uuid_generate_v4() $$ + LANGUAGE SQL VOLATILE; + SQL - @connection.transaction do - @connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| - t.string 'name' - t.uuid 'other_uuid', default: 'uuid_generate_v4()' - end + # Create such a table with custom function as default value generator + connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t| + t.string 'name' + t.uuid 'other_uuid_2', default: 'my_uuid_generator()' end end - def teardown - @connection.execute 'drop table if exists pg_uuids' + teardown do + drop_table "pg_uuids" + drop_table 'pg_uuids_2' + connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' end if ActiveRecord::Base.connection.supports_extensions? @@ -49,49 +164,46 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end def test_pk_and_sequence_for_uuid_primary_key - pk, seq = @connection.pk_and_sequence_for('pg_uuids') + pk, seq = connection.pk_and_sequence_for('pg_uuids') assert_equal 'id', pk assert_equal nil, seq end def test_schema_dumper_for_uuid_primary_key schema = StringIO.new - ActiveRecord::SchemaDumper.dump(@connection, schema) + ActiveRecord::SchemaDumper.dump(connection, schema) assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(connection, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string) + end end end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase - class UUID < ActiveRecord::Base - self.table_name = 'pg_uuids' - end + include PostgresqlUUIDHelper - def setup - @connection = ActiveRecord::Base.connection - @connection.reconnect! + setup do + enable_uuid_ossp!(connection) - unless @connection.extension_enabled?('uuid-ossp') - @connection.enable_extension 'uuid-ossp' - @connection.commit_db_transaction - end - - @connection.transaction do - @connection.create_table('pg_uuids', id: false) do |t| - t.primary_key :id, :uuid, default: nil - t.string 'name' - end + connection.create_table('pg_uuids', id: false) do |t| + t.primary_key :id, :uuid, default: nil + t.string 'name' end end - def teardown - @connection.execute 'drop table if exists pg_uuids' + teardown do + drop_table "pg_uuids" end if ActiveRecord::Base.connection.supports_extensions? def test_id_allows_default_override_via_nil - col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first @@ -101,6 +213,8 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase end class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase + include PostgresqlUUIDHelper + class UuidPost < ActiveRecord::Base self.table_name = 'pg_uuid_posts' has_many :uuid_comments, inverse_of: :uuid_post @@ -111,30 +225,24 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase belongs_to :uuid_post end - def setup - @connection = ActiveRecord::Base.connection - @connection.reconnect! - - unless @connection.extension_enabled?('uuid-ossp') - @connection.enable_extension 'uuid-ossp' - @connection.commit_db_transaction - end + setup do + enable_uuid_ossp!(connection) - @connection.transaction do - @connection.create_table('pg_uuid_posts', id: :uuid) do |t| + connection.transaction do + connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' end - @connection.create_table('pg_uuid_comments', id: :uuid) do |t| - t.uuid :uuid_post_id, default: 'uuid_generate_v4()' + connection.create_table('pg_uuid_comments', id: :uuid) do |t| + t.references :uuid_post, type: :uuid t.string 'content' end end end - def teardown - @connection.transaction do - @connection.execute 'drop table if exists pg_uuid_comments' - @connection.execute 'drop table if exists pg_uuid_posts' + teardown do + connection.transaction do + drop_table "pg_uuid_comments" + drop_table "pg_uuid_posts" end end diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb index 66e07b71a0..47b7d38eda 100644 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -1,11 +1,15 @@ require "cases/helper" -class ViewTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +module ViewTestConcern + extend ActiveSupport::Concern + + included do + self.use_transactional_fixtures = false + mattr_accessor :view_type + end SCHEMA_NAME = 'test_schema' TABLE_NAME = 'things' - VIEW_NAME = 'view_things' COLUMNS = [ 'id integer', 'name character varying(50)', @@ -14,17 +18,19 @@ class ViewTest < ActiveRecord::TestCase ] class ThingView < ActiveRecord::Base - self.table_name = 'test_schema.view_things' end def setup + super + ThingView.table_name = "#{SCHEMA_NAME}.#{view_type}_things" + @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" - @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})" - @connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}" + @connection.execute "CREATE #{view_type.humanize} #{ThingView.table_name} AS SELECT * FROM #{SCHEMA_NAME}.#{TABLE_NAME}" end def teardown + super @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" end @@ -35,7 +41,7 @@ class ViewTest < ActiveRecord::TestCase def test_column_definitions assert_nothing_raised do - assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}") + assert_equal COLUMNS, columns(ThingView.table_name) end end @@ -47,3 +53,15 @@ class ViewTest < ActiveRecord::TestCase end end + +class ViewTest < ActiveRecord::TestCase + include ViewTestConcern + self.view_type = 'view' +end + +if ActiveRecord::Base.connection.supports_materialized_views? + class MaterializedViewTest < ActiveRecord::TestCase + include ViewTestConcern + self.view_type = 'materialized_view' + end +end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index dd2a727afe..4165dd5ac9 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require 'cases/helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlXMLTest < ActiveRecord::TestCase class XmlDataType < ActiveRecord::Base @@ -14,16 +11,16 @@ class PostgresqlXMLTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('xml_data_type') do |t| - t.xml 'payload', default: {} + t.xml 'payload' end end rescue ActiveRecord::StatementInvalid skip "do not test on PG without xml" end - @column = XmlDataType.columns.find { |c| c.name == 'payload' } + @column = XmlDataType.columns_hash['payload'] end - def teardown + teardown do @connection.execute 'drop table if exists xml_data_type' end @@ -35,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| assert_nil XmlDataType.first.payload end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index b478db749d..13b754d226 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type assert_equal original_id.limit, copied_id.limit - assert_equal original_id.primary, copied_id.primary end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index b227bce680..f1d6119d2e 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -9,15 +9,15 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ba89487838..ac8332e2fa 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,7 +15,7 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Struct.new(:type, :name).new(:string, "foo") + column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) assert_equal expected, @conn.type_cast(binary, column) @@ -47,13 +47,13 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 't', @conn.type_cast(true, nil) assert_equal 1, @conn.type_cast(true, c) end def test_type_cast_false - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 'f', @conn.type_cast(false, nil) assert_equal 0, @conn.type_cast(false, c) end @@ -61,16 +61,16 @@ module ActiveRecord def test_type_cast_string assert_equal '10', @conn.type_cast('10', nil) - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 10, @conn.type_cast('10', c) - c = Column.new(nil, 1, 'float') + c = Column.new(nil, 1, Type::Float.new) assert_equal 10.1, @conn.type_cast('10.1', c) - c = Column.new(nil, 1, 'binary') + c = Column.new(nil, 1, Type::Binary.new) assert_equal '10.1', @conn.type_cast('10.1', c) - c = Column.new(nil, 1, 'date') + c = Column.new(nil, 1, Type::Date.new) assert_equal '10.1', @conn.type_cast('10.1', c) end @@ -84,7 +84,7 @@ module ActiveRecord assert_raise(TypeError) { @conn.type_cast(obj, nil) } end - def test_quoted_id + def test_type_cast_object_which_responds_to_quoted_id quoted_id_obj = Class.new { def quoted_id "'zomg'" @@ -100,9 +100,16 @@ module ActiveRecord def quoted_id "'zomg'" end - } + }.new assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + column = Column.new(nil, 1, SQLite3String.new) + + assert_equal "'hello'", @conn.quote(value, column) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 02834edf7b..b2bf9480dd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -2,28 +2,22 @@ require "cases/helper" require 'models/owner' require 'tempfile' +require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + include DdlHelper + self.use_transactional_fixtures = false class DualEncoding < ActiveRecord::Base end def setup - @conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 - @conn.execute <<-eosql - CREATE TABLE items ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - - @subscriber = SQLSubscriber.new - ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) + @conn = Base.sqlite3_connection database: ':memory:', + adapter: 'sqlite3', + timeout: 100 end def test_bad_connection @@ -33,12 +27,11 @@ module ActiveRecord end end - def test_connect_with_url - skip "can't establish new connection when using memory db" if in_memory_db? - begin + unless in_memory_db? + def test_connect_with_url original_connection = ActiveRecord::Base.remove_connection tf = Tempfile.open 'whatever' - url = "sqlite3://#{tf.path}" + url = "sqlite3:#{tf.path}" ActiveRecord::Base.establish_connection(url) assert ActiveRecord::Base.connection ensure @@ -46,13 +39,10 @@ module ActiveRecord tf.unlink ActiveRecord::Base.establish_connection(original_connection) end - end - def test_connect_memory_with_url - skip "can't establish new connection when using memory db" if in_memory_db? - begin + def test_connect_memory_with_url original_connection = ActiveRecord::Base.remove_connection - url = "sqlite3:///:memory:" + url = "sqlite3::memory:" ActiveRecord::Base.establish_connection(url) assert ActiveRecord::Base.connection ensure @@ -61,25 +51,23 @@ module ActiveRecord end def test_valid_column - column = @conn.columns('items').find { |col| col.name == 'id' } - assert @conn.valid_type?(column.type) + with_example_table do + column = @conn.columns('ex').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end end - # sqlite databases should be able to support any type and not - # just the ones mentioned in the native_database_types. - # Therefore test_invalid column should always return true - # even if the type is not valid. + # sqlite3 databases should be able to support any type and not just the + # ones mentioned in the native_database_types. + # + # Therefore test_invalid column should always return true even if the + # type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end - def teardown - ActiveSupport::Notifications.unsubscribe(@subscriber) - super - end - def test_column_types - owner = Owner.create!(:name => "hello".encode('ascii-8bit')) + owner = Owner.create!(name: "hello".encode('ascii-8bit')) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', ' result = Owner.connection.exec_query <<-esql @@ -89,23 +77,28 @@ module ActiveRecord esql assert(!result.rows.first.include?("blob"), "should not store blobs") + ensure + owner.delete end def test_exec_insert - column = @conn.columns('items').find { |col| col.name == 'number' } - vals = [[column, 10]] - @conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals) + with_example_table do + column = @conn.columns('ex').find { |col| col.name == 'number' } + vals = [[column, 10]] + @conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals) - result = @conn.exec_query( - 'select number from items where number = ?', 'SQL', vals) + result = @conn.exec_query( + 'select number from ex where number = ?', 'SQL', vals) - assert_equal 1, result.rows.length - assert_equal 10, result.rows.first.first + assert_equal 1, result.rows.length + assert_equal 10, result.rows.first.first + end end def test_primary_key_returns_nil_for_no_pk - @conn.exec_query('create table ex(id int, data string)') - assert_nil @conn.primary_key('ex') + with_example_table 'id int, data string' do + assert_nil @conn.primary_key('ex') + end end def test_connection_no_db @@ -116,17 +109,17 @@ module ActiveRecord def test_bad_timeout assert_raises(TypeError) do - Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 'usa' + Base.sqlite3_connection database: ':memory:', + adapter: 'sqlite3', + timeout: 'usa' end end # connection is OK with a nil timeout def test_nil_timeout - conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => nil + conn = Base.sqlite3_connection database: ':memory:', + adapter: 'sqlite3', + timeout: nil assert conn, 'made a connection' end @@ -145,77 +138,83 @@ module ActiveRecord end def test_exec_no_binds - @conn.exec_query('create table ex(id int, data string)') - result = @conn.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns - - @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - result = @conn.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows + with_example_table 'id int, data string' do + result = @conn.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end end def test_exec_query_with_binds - @conn.exec_query('create table ex(id int, data string)') - @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - result = @conn.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + with_example_table 'id int, data string' do + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [[1, 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end def test_exec_query_typecasts_bind_vals - @conn.exec_query('create table ex(id int, data string)') - @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - column = @conn.columns('ex').find { |col| col.name == 'id' } + with_example_table 'id int, data string' do + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @conn.columns('ex').find { |col| col.name == 'id' } - result = @conn.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + result = @conn.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [[1, 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end def test_quote_binary_column_escapes_it DualEncoding.connection.execute(<<-eosql) - CREATE TABLE dual_encodings ( + CREATE TABLE IF NOT EXISTS dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, name varchar(255), data binary ) eosql str = "\x80".force_encoding("ASCII-8BIT") - binary = DualEncoding.new :name => 'いただきます!', :data => str + binary = DualEncoding.new name: 'いただきます!', data: str binary.save! assert_equal str, binary.data - ensure - DualEncoding.connection.drop_table('dual_encodings') + DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings') end def test_type_cast_should_not_mutate_encoding name = 'hello'.force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding + ensure + Owner.delete_all end def test_execute - @conn.execute "INSERT INTO items (number) VALUES (10)" - records = @conn.execute "SELECT * FROM items" - assert_equal 1, records.length - - record = records.first - assert_equal 10, record['number'] - assert_equal 1, record['id'] + with_example_table do + @conn.execute "INSERT INTO ex (number) VALUES (10)" + records = @conn.execute "SELECT * FROM ex" + assert_equal 1, records.length + + record = records.first + assert_equal 10, record['number'] + assert_equal 1, record['id'] + end end def test_quote_string @@ -223,128 +222,141 @@ module ActiveRecord end def test_insert_sql - 2.times do |i| - rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})" - assert_equal(i + 1, rv) + with_example_table do + 2.times do |i| + rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})" + assert_equal(i + 1, rv) + end + + records = @conn.execute "SELECT * FROM ex" + assert_equal 2, records.length end - - records = @conn.execute "SELECT * FROM items" - assert_equal 2, records.length end def test_insert_sql_logged - sql = "INSERT INTO items (number) VALUES (10)" - name = "foo" - - assert_logged([[sql, name, []]]) do - @conn.insert_sql sql, name + with_example_table do + sql = "INSERT INTO ex (number) VALUES (10)" + name = "foo" + assert_logged [[sql, name, []]] do + @conn.insert_sql sql, name + end end end def test_insert_id_value_returned - sql = "INSERT INTO items (number) VALUES (10)" - idval = 'vuvuzela' - id = @conn.insert_sql sql, nil, nil, idval - assert_equal idval, id + with_example_table do + sql = "INSERT INTO ex (number) VALUES (10)" + idval = 'vuvuzela' + id = @conn.insert_sql sql, nil, nil, idval + assert_equal idval, id + end end def test_select_rows - 2.times do |i| - @conn.create "INSERT INTO items (number) VALUES (#{i})" + with_example_table do + 2.times do |i| + @conn.create "INSERT INTO ex (number) VALUES (#{i})" + end + rows = @conn.select_rows 'select number, id from ex' + assert_equal [[0, 1], [1, 2]], rows end - rows = @conn.select_rows 'select number, id from items' - assert_equal [[0, 1], [1, 2]], rows end def test_select_rows_logged - sql = "select * from items" - name = "foo" - - assert_logged([[sql, name, []]]) do - @conn.select_rows sql, name + with_example_table do + sql = "select * from ex" + name = "foo" + assert_logged [[sql, name, []]] do + @conn.select_rows sql, name + end end end def test_transaction - count_sql = 'select count(*) from items' + with_example_table do + count_sql = 'select count(*) from ex' - @conn.begin_db_transaction - @conn.create "INSERT INTO items (number) VALUES (10)" + @conn.begin_db_transaction + @conn.create "INSERT INTO ex (number) VALUES (10)" - assert_equal 1, @conn.select_rows(count_sql).first.first - @conn.rollback_db_transaction - assert_equal 0, @conn.select_rows(count_sql).first.first + assert_equal 1, @conn.select_rows(count_sql).first.first + @conn.rollback_db_transaction + assert_equal 0, @conn.select_rows(count_sql).first.first + end end def test_tables - assert_equal %w{ items }, @conn.tables - - @conn.execute <<-eosql - CREATE TABLE people ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - assert_equal %w{ items people }.sort, @conn.tables.sort + with_example_table do + assert_equal %w{ ex }, @conn.tables + with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do + assert_equal %w{ ex people }.sort, @conn.tables.sort + end + end end def test_tables_logs_name - assert_logged [['SCHEMA', []]] do + sql = <<-SQL + SELECT name FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + assert_logged [[sql.squish, 'SCHEMA', []]] do @conn.tables('hello') - assert_not_nil @subscriber.logged.first.shift end end def test_indexes_logs_name - assert_logged [["PRAGMA index_list(\"items\")", 'SCHEMA', []]] do - @conn.indexes('items', 'hello') + with_example_table do + assert_logged [["PRAGMA index_list(\"ex\")", 'SCHEMA', []]] do + @conn.indexes('ex', 'hello') + end end end def test_table_exists_logs_name - assert @conn.table_exists?('items') - assert_equal 'SCHEMA', @subscriber.logged[0][1] + with_example_table do + sql = <<-SQL + SELECT name FROM sqlite_master + WHERE type = 'table' + AND NOT name = 'sqlite_sequence' AND name = \"ex\" + SQL + assert_logged [[sql.squish, 'SCHEMA', []]] do + assert @conn.table_exists?('ex') + end + end end def test_columns - columns = @conn.columns('items').sort_by { |x| x.name } - assert_equal 2, columns.length - assert_equal %w{ id number }.sort, columns.map { |x| x.name } - assert_equal [nil, nil], columns.map { |x| x.default } - assert_equal [true, true], columns.map { |x| x.null } + with_example_table do + columns = @conn.columns('ex').sort_by { |x| x.name } + assert_equal 2, columns.length + assert_equal %w{ id number }.sort, columns.map { |x| x.name } + assert_equal [nil, nil], columns.map { |x| x.default } + assert_equal [true, true], columns.map { |x| x.null } + end end def test_columns_with_default - @conn.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer default 10 - ) - eosql - column = @conn.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert_equal 10, column.default + with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer default 10' do + column = @conn.columns('ex').find { |x| + x.name == 'number' + } + assert_equal '10', column.default + end end def test_columns_with_not_null - @conn.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - column = @conn.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert !column.null, "column should not be null" + with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer not null' do + column = @conn.columns('ex').find { |x| x.name == 'number' } + assert_not column.null, "column should not be null" + end end def test_indexes_logs - assert_difference('@subscriber.logged.length') do - @conn.indexes('items') + with_example_table do + assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do + @conn.indexes('ex') + end end - assert_match(/items/, @subscriber.logged.last.first) end def test_no_indexes @@ -352,41 +364,45 @@ module ActiveRecord end def test_index - @conn.add_index 'items', 'id', :unique => true, :name => 'fun' - index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + with_example_table do + @conn.add_index 'ex', 'id', unique: true, name: 'fun' + index = @conn.indexes('ex').find { |idx| idx.name == 'fun' } - assert_equal 'items', index.table - assert index.unique, 'index is unique' - assert_equal ['id'], index.columns + assert_equal 'ex', index.table + assert index.unique, 'index is unique' + assert_equal ['id'], index.columns + end end def test_non_unique_index - @conn.add_index 'items', 'id', :name => 'fun' - index = @conn.indexes('items').find { |idx| idx.name == 'fun' } - assert !index.unique, 'index is not unique' + with_example_table do + @conn.add_index 'ex', 'id', name: 'fun' + index = @conn.indexes('ex').find { |idx| idx.name == 'fun' } + assert_not index.unique, 'index is not unique' + end end def test_compound_index - @conn.add_index 'items', %w{ id number }, :name => 'fun' - index = @conn.indexes('items').find { |idx| idx.name == 'fun' } - assert_equal %w{ id number }.sort, index.columns.sort + with_example_table do + @conn.add_index 'ex', %w{ id number }, name: 'fun' + index = @conn.indexes('ex').find { |idx| idx.name == 'fun' } + assert_equal %w{ id number }.sort, index.columns.sort + end end def test_primary_key - assert_equal 'id', @conn.primary_key('items') - - @conn.execute <<-eosql - CREATE TABLE foos ( - internet integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - assert_equal 'internet', @conn.primary_key('foos') + with_example_table do + assert_equal 'id', @conn.primary_key('ex') + with_example_table 'internet integer PRIMARY KEY AUTOINCREMENT, number integer not null', 'foos' do + assert_equal 'internet', @conn.primary_key('foos') + end + end end def test_no_primary_key - @conn.execute 'CREATE TABLE failboat (number integer not null)' - assert_nil @conn.primary_key('failboat') + with_example_table 'number integer not null' do + assert_nil @conn.primary_key('ex') + end end def test_supports_extensions @@ -401,13 +417,39 @@ module ActiveRecord assert @conn.respond_to?(:disable_extension) end + def test_statement_closed + db = SQLite3::Database.new(ActiveRecord::Base. + configurations['arunit']['database']) + statement = SQLite3::Statement.new(db, + 'CREATE TABLE statement_test (number integer not null)') + statement.stubs(:step).raises(SQLite3::BusyException, 'busy') + statement.stubs(:columns).once.returns([]) + statement.expects(:close).once + SQLite3::Statement.stubs(:new).returns(statement) + + assert_raises ActiveRecord::StatementInvalid do + @conn.exec_query 'select * from statement_test' + end + end + private def assert_logged logs + subscriber = SQLSubscriber.new + subscription = ActiveSupport::Notifications.subscribe('sql.active_record', subscriber) yield - assert_equal logs, @subscriber.logged + assert_equal logs, subscriber.logged + ensure + ActiveSupport::Notifications.unsubscribe(subscription) end + def with_example_table(definition = nil, table_name = 'ex', &block) + definition ||= <<-SQL + id integer PRIMARY KEY AUTOINCREMENT, + number integer + SQL + super(@conn, table_name, definition, &block) + end end end end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 500df52cd8..8700b20dee 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -10,13 +10,27 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::SchemaMigration.drop_table end - def teardown + teardown do @connection.drop_table :fruits rescue nil @connection.drop_table :nep_fruits rescue nil @connection.drop_table :nep_schema_migrations rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil end + def test_has_no_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_nil ActiveRecord::SchemaMigration.primary_key + + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 + end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end + def test_schema_define ActiveRecord::Schema.define(:version => 7) do create_table :fruits do |t| @@ -34,6 +48,7 @@ if ActiveRecord::Base.connection.supports_migrations? def test_schema_define_w_table_name_prefix table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix ActiveRecord::Base.table_name_prefix = "nep_" ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" ActiveRecord::Schema.define(:version => 7) do @@ -46,7 +61,7 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_equal 7, ActiveRecord::Migrator::current_version ensure - ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name end @@ -66,5 +81,12 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } end + + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end end end diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index c78b036f53..3e0032ec73 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -9,7 +9,12 @@ module ActiveRecord scope = AssociationScope.scope(Author.new.association(:welcome_posts), Author.connection) wheres = scope.where_values.map(&:right) + binds = scope.bind_values.map(&:last) + wheres = scope.where_values.map(&:right).reject { |node| + Arel::Nodes::BindParam === node + } assert_equal wheres.uniq, wheres + assert_equal binds.uniq, binds end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9340bc0a83..25555bd75c 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -16,6 +16,8 @@ require 'models/essay' require 'models/toy' require 'models/invoice' require 'models/line_item' +require 'models/column' +require 'models/record' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -231,13 +233,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_counter debate = Topic.create("title" => "debate") - assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet" + assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" trash = debate.replies.create("title" => "blah!", "content" => "world around!") - assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created" + assert_equal 1, Topic.find(debate.id).read_attribute("replies_count"), "First reply created" trash.destroy - assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" + assert_equal 0, Topic.find(debate.id).read_attribute("replies_count"), "First reply deleted" end def test_belongs_to_counter_with_assigning_nil @@ -340,6 +342,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes + assert_not LineItem.column_names.include?("updated_at") + + line_item = LineItem.create! + invoice = Invoice.create!(line_items: [line_item]) + initial = invoice.updated_at + line_item.touch + + assert_not_equal initial, invoice.reload.updated_at + end + def test_belongs_to_with_touch_option_on_touch_and_removed_parent line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -356,6 +369,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(2) { line_item.update amount: 10 } end + def test_belongs_to_with_touch_option_on_empty_update + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(0) { line_item.save } + end + def test_belongs_to_with_touch_option_on_destroy line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -489,6 +509,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 4, topic.replies.size end + def test_concurrent_counter_cache_double_destroy + topic = Topic.create :title => "Zoom-zoom-zoom" + + 5.times do + topic.replies.create(:title => "re: zoom", :content => "speedy quick!") + end + + assert_equal 5, topic.reload[:replies_count] + assert_equal 5, topic.replies.size + + reply = topic.replies.first + reply_clone = Reply.find(reply.id) + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + + reply_clone.destroy + assert_equal 4, topic.reload[:replies_count] + assert_equal 4, topic.replies.size + end + def test_custom_counter_cache reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") assert_equal 0, reply[:replies_count] @@ -529,6 +570,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert companies(:first_client).readonly_firm.readonly? end + def test_test_polymorphic_assignment_foreign_key_type_string + comment = Comment.first + comment.author = Author.first + comment.resource = Member.first + comment.save + + assert_equal Comment.all.to_a, + Comment.includes(:author).to_a + + assert_equal Comment.all.to_a, + Comment.includes(:resource).to_a + end + def test_polymorphic_assignment_foreign_type_field_updating # should update when assigning a saved record sponsor = Sponsor.new @@ -733,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.taggings_count }, -1 do - assert_difference 'comment.reload.taggings_count', +1 do + assert_difference lambda { post.reload.tags_count }, -1 do + assert_difference 'comment.reload.tags_count', +1 do tagging.taggable = comment end end @@ -824,6 +878,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 0, comments(:greetings).reload.children_count end + def test_belongs_to_with_id_assigning + post = posts(:welcome) + comment = Comment.create! body: "foo", post: post + parent = comments(:greetings) + assert_equal 0, parent.reload.children_count + comment.parent_id = parent.id + + comment.save! + assert_equal 1, parent.reload.children_count + end + def test_polymorphic_with_custom_primary_key toy = Toy.create! sponsor = Sponsor.create!(:sponsorable => toy) @@ -863,4 +928,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end end end + + test 'belongs_to works with model called Record' do + record = Record.create! + Column.create! record: record + assert_equal 1, Column.count + end +end + +class BelongsToWithForeignKeyTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses + + def test_destroy_linked_models + address = AuthorAddress.create! + author = Author.create! name: "Author", author_address_id: address.id + + author.destroy! + end end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 2d0d4541b4..5b7e462f64 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -101,6 +101,27 @@ class AssociationCallbacksTest < ActiveRecord::TestCase "after_adding#{david.id}"], ar.developers_log end + def test_has_and_belongs_to_many_before_add_called_before_save + dev = nil + new_dev = nil + klass = Class.new(Project) do + def self.name; Project.name; end + has_and_belongs_to_many :developers_with_callbacks, + :class_name => "Developer", + :before_add => lambda { |o,r| + dev = r + new_dev = r.new_record? + } + end + rec = klass.create! + alice = Developer.new(:name => 'alice') + rec.developers_with_callbacks << alice + assert_equal alice, dev + assert_not_nil new_dev + assert new_dev, "record should not have been saved" + assert_not alice.new_record? + end + def test_has_and_belongs_to_many_after_add_called_after_save ar = projects(:active_record) assert ar.developers_log.empty? @@ -129,7 +150,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase "after_removing#{jamis.id}"], activerecord.developers_log end - def test_has_and_belongs_to_many_remove_callback_on_clear + def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear activerecord = projects(:active_record) assert activerecord.developers_log.empty? if activerecord.developers_with_callbacks.size == 0 @@ -138,9 +159,9 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort + activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort assert activerecord.developers_with_callbacks.clear - assert_equal log_array, activerecord.developers_log.sort + assert_predicate activerecord.developers_log, :empty? end def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 71c0609df5..51d8e0523e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb new file mode 100644 index 0000000000..48f7ddbe83 --- /dev/null +++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :taggings, as: :taggable + has_many :tags, through: :taggings + end + + class Tagging < ActiveRecord::Base + belongs_to :taggable, polymorphic: true + belongs_to :tag + end + + class Tag < ActiveRecord::Base + end + + test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do + post = Post.create!(title: 'Hello', body: 'World!') + assert_deprecated { post.tags << Tag.create!(name: 'whatever') } + + assert_equal 1, post.tags.size + assert_equal 1, post.tags_count + assert_equal 1, post.reload.tags.size + assert_equal 1, post.reload.tags_count + 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 5ff117eaa0..0ff87d53ea 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -68,7 +68,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase generate_test_object_graphs end - def teardown + teardown do [Circle, Square, Triangle, PaintColor, PaintTexture, ShapeExpression, NonPolyOne, NonPolyTwo].each do |c| c.delete_all @@ -111,7 +111,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase @first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post) end - def teardown + teardown do @davey_mcdave.destroy @first_post.destroy @first_comment.destroy diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 669569a774..a61a070331 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,6 +1,7 @@ require "cases/helper" +if ActiveRecord::Base.connection.supports_migrations? class EagerSingularizationTest < ActiveRecord::TestCase class Virus < ActiveRecord::Base belongs_to :octopus @@ -50,8 +51,6 @@ class EagerSingularizationTest < ActiveRecord::TestCase end def setup - skip 'Does not support migrations' unless connection.supports_migrations? - connection.create_table :viri do |t| t.column :octopus_id, :integer t.column :species, :string @@ -91,7 +90,7 @@ class EagerSingularizationTest < ActiveRecord::TestCase end end - def teardown + teardown do connection.drop_table :viri connection.drop_table :octopi connection.drop_table :passes @@ -146,3 +145,4 @@ class EagerSingularizationTest < ActiveRecord::TestCase end end end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 498a4e8144..21912fdf0f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -235,6 +235,17 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_finding_with_includes_on_empty_polymorphic_type_column + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + sponsor.update!(sponsorable_type: '', sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL + sponsor = assert_queries(1) do + assert_nothing_raised { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) } + end + assert_no_queries do + assert_equal nil, sponsor.sponsorable + end + end + def test_loading_from_an_association posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a assert_equal 2, posts.first.comments.size @@ -407,19 +418,19 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_load_has_one_quotes_table_and_column_names - michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael)) + michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael).id) references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference} end def test_eager_load_has_many_quotes_table_and_column_names - michael = Person.all.merge!(:includes => :references).find(people(:michael)) + michael = Person.all.merge!(:includes => :references).find(people(:michael).id) references(:michael_magician,:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names - michael = Person.all.merge!(:includes => :jobs).find(people(:michael)) + michael = Person.all.merge!(:includes => :jobs).find(people(:michael).id) jobs(:magician, :unicyclist) assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } end @@ -523,21 +534,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions - if current_adapter?(:OpenBaseAdapter) - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a - else - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a - end + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } end def test_eager_with_has_many_and_limit_and_conditions_array - if current_adapter?(:OpenBaseAdapter) - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a - else - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a - end + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } end @@ -709,16 +712,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_invalid_association_reference - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=> :monkeys ).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=>[ :monkeys ]).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=>[ 'monkeys' ]).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6) } end @@ -815,11 +818,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_with_interpolation - post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + assert_deprecated do + post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end - post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + assert_deprecated do + post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end end def test_polymorphic_type_condition @@ -925,13 +932,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_count_with_include - if current_adapter?(:SybaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count - elsif current_adapter?(:OpenBaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count - else - assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count - end + assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end def test_load_with_sti_sharing_association @@ -1166,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end + test "deep preload" do + post = Post.preload(author: :posts, comments: :post).first + + assert_predicate post.author.association(:posts), :loaded? + assert_predicate post.comments.first.association(:post), :loaded? + end + test "preloading does not cache has many association subset when preloaded with a through association" do author = Author.includes(:comments_with_order_and_conditions, :posts).first assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } @@ -1187,6 +1195,23 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:bob), author end + test "preloading with a polymorphic association and using the existential predicate but also using a select" do + assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer + + assert_nothing_raised do + authors(:david).essays.includes(:writer).select(:name).any? + end + end + + test "preloading the same association twice works" do + Member.create! + members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a + assert_no_queries { + members_with_membership = members.select(&:current_membership) + assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size + } + end + test "preloading with a polymorphic association and using the existential predicate" do assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer @@ -1194,4 +1219,75 @@ class EagerAssociationTest < ActiveRecord::TestCase authors(:david).essays.includes(:writer).any? end end + + test "preloading associations with string joins and order references" do + author = assert_queries(2) { + Author.includes(:posts).joins("LEFT JOIN posts ON posts.author_id = authors.id").order("posts.title DESC").first + } + assert_no_queries { + assert_equal 5, author.posts.size + } + end + + test "including associations with where.not adds implicit references" do + author = assert_queries(2) { + Author.includes(:posts).where.not(posts: { title: 'Welcome to the weblog'} ).last + } + + assert_no_queries { + assert_equal 2, author.posts.size + } + end + + test "including association based on sql condition and no database column" do + assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet + end + + test "include instance dependent associations is deprecated" do + message = "association scope 'posts_with_signature' is" + assert_deprecated message do + begin + Author.includes(:posts_with_signature).to_a + rescue NoMethodError + # it's expected that preloading of this association fails + end + end + + assert_deprecated message do + Author.preload(:posts_with_signature).to_a rescue NoMethodError + end + + assert_deprecated message do + Author.eager_load(:posts_with_signature).to_a + end + end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + skip "eager_load does not yet preserve readonly associations" + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index bac1cb8e2d..cc58a4a1a2 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 @@ -11,6 +11,7 @@ require 'models/author' require 'models/tag' require 'models/tagging' require 'models/parrot' +require 'models/person' require 'models/pirate' require 'models/treasure' require 'models/price_estimate' @@ -20,6 +21,10 @@ require 'models/membership' require 'models/sponsor' require 'models/country' require 'models/treaty' +require 'models/vertex' +require 'models/publisher' +require 'models/publisher/article' +require 'models/publisher/magazine' require 'active_support/core_ext/string/conversions' class ProjectWithAfterCreateHook < ActiveRecord::Base @@ -65,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class SubDeveloper < Developer + self.table_name = 'developers' + has_and_belongs_to_many :special_projects, + :join_table => 'developers_projects', + :foreign_key => "project_id", + :association_foreign_key => "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -81,6 +94,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase country.treaties << treaty end + def test_marshal_dump + post = posts :welcome + preloaded = Post.includes(:categories).find post.id + assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) + end + def test_should_property_quote_string_primary_keys setup_data_for_habtm_case @@ -215,6 +234,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers end + def test_habtm_collection_size_from_build + devel = Developer.create("name" => "Fred Wu") + devel.projects << Project.create("name" => "Grimetime") + devel.projects.build + + assert_equal 2, devel.projects.size + end + + def test_habtm_collection_size_from_params + devel = Developer.new({ + projects_attributes: { + '0' => {} + } + }) + + assert_equal 1, devel.projects.size + end + def test_build devel = Developer.find(1) proj = assert_no_queries { devel.projects.build("name" => "Projekt") } @@ -785,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Pirate.where(id: redbeard.id) end - test "has and belongs to many associations on new records use null relations" do + def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects assert_no_queries do assert_equal [], projects @@ -795,4 +832,55 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end end + def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_create + rich_person = RichPerson.new + + treasure = Treasure.new + treasure.rich_people << rich_person + treasure.valid? + + assert_equal 1, treasure.rich_people.size + assert_nil rich_person.first_name, 'should not run associated person validation on create when validate: false' + end + + def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_update + rich_person = RichPerson.create! + person_first_name = rich_person.first_name + assert_not_nil person_first_name + + treasure = Treasure.new + treasure.rich_people << rich_person + treasure.valid? + + assert_equal 1, treasure.rich_people.size + assert_equal person_first_name, rich_person.first_name, 'should not run associated person validation on update when validate: false' + end + + def test_custom_join_table + assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table + end + + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model + magazine = Publisher::Magazine.create + article = Publisher::Article.create + magazine.articles << article + magazine.save + + assert_includes magazine.articles, article + end + + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model + article = Publisher::Article.create + tag = Tag.create + article.tags << tag + article.save + + assert_includes article.tags, tag + end + + def test_redefine_habtm + child = SubDeveloper.new("name" => "Aredridel") + child.special_projects << SpecialProject.new("name" => "Special Project") + assert child.save, 'child object should be saved' + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 321440cab7..fe961e871c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -22,6 +22,10 @@ require 'models/engine' require 'models/categorization' require 'models/minivan' require 'models/speedometer' +require 'models/reference' +require 'models/job' +require 'models/college' +require 'models/student' require 'models/pirate' require 'models/ship' @@ -32,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa 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 + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last end end @@ -41,12 +45,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations + :categorizations, :jobs, :tags def setup Client.destroyed_client_ids.clear end + def test_sti_subselect_count + tag = Tag.first + len = Post.tagged_with(tag.id).limit(10).size + assert_operator len, :>, 0 + end + def test_anonymous_has_many developer = Class.new(ActiveRecord::Base) { self.table_name = 'developers' @@ -65,6 +75,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase dev.developer_projects.map(&:project_id).sort end + def test_has_many_build_with_options + college = College.create(name: 'UFMT') + Student.create(active: true, college_id: college.id, name: 'Sarah') + + assert_equal college.students, Student.where(active: true, college_id: college.id) + end + def test_create_from_association_should_respect_default_scope car = Car.create(:name => 'honda') assert_equal 'honda', car.name @@ -109,6 +126,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy" end + def test_delete_all_on_association_is_the_same_as_not_loaded + author = authors :david + author.thinking_posts.create!(:body => "test") + author.reload + expected_sql = capture_sql { author.thinking_posts.delete_all } + + author.thinking_posts.create!(:body => "test") + author.reload + author.thinking_posts.inspect + loaded_sql = capture_sql { author.thinking_posts.delete_all } + assert_equal(expected_sql, loaded_sql) + end + + def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded + author = authors :david + author.posts.create!(:title => "test", :body => "body") + author.reload + expected_sql = capture_sql { author.posts.delete_all } + + author.posts.create!(:title => "test", :body => "body") + author.reload + author.posts.to_a + loaded_sql = capture_sql { author.posts.delete_all } + assert_equal(expected_sql, loaded_sql) + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build @@ -729,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end + def test_counter_cache_updates_in_memory_after_concat + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create_with_array + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!([ + { title: "re: zoom", content: "speedy quick!" }, + { title: "re: zoom 2", content: "OMG lol!" }, + ]) + assert_equal 2, topic.replies_count + assert_equal 2, topic.replies.size + assert_equal 2, topic.reload.replies.size + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -741,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) - assert_difference "post.reload.taggings_count", -1 do + assert_difference "post.reload.tags_count", -1 do post.taggings.delete(post.taggings.first) end end def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_columns(taggings_with_delete_all_count: post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.tags_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -757,13 +830,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_columns(taggings_with_destroy_count: post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.tags_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) end end + def test_calling_empty_with_counter_cache + post = posts(:welcome) + assert_queries(0) do + assert_not post.comments.empty? + end + end + def test_custom_named_counter_cache topic = topics(:first) @@ -1244,6 +1324,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal orig_accounts, firm.accounts end + def test_replace_with_same_content + firm = Firm.first + firm.clients = [] + firm.save + + assert_queries(0, ignore_none: true) do + firm.clients = [] + end + end + def test_transactions_when_replacing_on_persisted good = Client.new(:name => "Good") bad = Client.new(:name => "Bad", :raise_on_save => true) @@ -1833,11 +1923,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - test 'has_many_association passes context validation to validate children' do + test 'passes custom context validation to validate children' do pirate = FamousPirate.new pirate.famous_ships << ship = FamousShip.new - assert_equal true, pirate.valid? - assert_equal false, pirate.valid?(:conference) + + assert pirate.valid? + assert_not pirate.valid?(:conference) assert_equal "can't be blank", ship.errors[:name].first end + + test 'association with instance dependent scope' do + bob = authors(:bob) + Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob)) + Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob)) + assert_equal ["misc post by bob", "other post by bob", + "signed post by bob"], bob.posts_with_signature.map(&:title).sort + + assert_equal [], authors(:david).posts_with_signature.map(&:title) + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 47592f312e..a85e020f0c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.single_people.include?(person) end + def test_both_parent_ids_set_when_saving_new + post = Post.new(title: 'Hello', body: 'world') + person = Person.new(first_name: 'Sean') + + post.people = [person] + post.save + + assert post.id + assert person.id + assert_equal post.id, post.readers.first.post_id + assert_equal person.id, post.readers.first.person_id + end + def test_delete_association assert_queries(2){posts(:welcome);people(:michael); } @@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + assert_difference ['post.reload.tags_count'], -1 do posts(:welcome).tags.delete(tag) end end @@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_destroy_count: post.tags.count) - assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + assert_difference ['post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) end end @@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_nullify_count: post.tags.count) - assert_no_difference 'post.reload.taggings_count' do + assert_no_difference 'post.reload.tags_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do posts(:welcome).tags_with_nullify.delete(tag) end @@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag.tagged_posts = [] post.reload - assert_equal(post.taggings.count, post.taggings_count) + assert_equal(post.taggings.count, post.tags_count) end def test_update_counter_caches_on_destroy post = posts(:welcome) tag = post.tags.create!(name: 'doomed') - assert_difference 'post.reload.taggings_count', -1 do + assert_difference 'post.reload.tags_count', -1 do tag.tagged_posts.destroy(post) end end @@ -698,9 +711,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :before, "Roger"], [:added, :after, "Roger"] ], log.last(4) - - post.people_with_callbacks.clear - assert_equal((%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort) end def test_dynamic_find_should_respect_association_include @@ -817,6 +827,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert author.named_categories(true).include?(category) end + def test_collection_exists + author = authors(:mary) + category = Category.create!(author_ids: [author.id], name: "Primary") + assert category.authors.exists?(id: author.id) + assert category.reload.authors.exists?(id: author.id) + end + def test_collection_delete_with_nonstandard_primary_key_on_belongs_to author = authors(:mary) category = author.named_categories.create(:name => "Primary") @@ -1075,7 +1092,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } end - test "has many through associations on new records use null relations" do + def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new assert_no_queries do @@ -1087,7 +1104,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - test "has many through with default scope on the target" do + def test_has_many_through_with_default_scope_on_the_target person = people(:michael) assert_equal [posts(:thinking)], person.first_posts @@ -1098,4 +1115,55 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_with_includes_in_through_association_scope assert_not_empty posts(:welcome).author_address_extra_with_address end + + def test_insert_records_via_has_many_through_association_with_scope + club = Club.create! + member = Member.create! + Membership.create!(club: club, member: member) + + club.favourites << member + assert_equal [member], club.favourites + + club.reload + assert_equal [member], club.favourites + end + + def test_has_many_through_unscope_default_scope + post = Post.create!(:title => 'Beaches', :body => "I like beaches!") + Reader.create! :person => people(:david), :post => post + LazyReader.create! :person => people(:susan), :post => post + + assert_equal 2, post.people.to_a.size + assert_equal 1, post.lazy_people.to_a.size + + assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size + assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size + end + + def test_has_many_through_add_with_sti_middle_relation + club = SuperClub.create!(name: 'Fight Club') + member = Member.create!(name: 'Tyler Durden') + + club.members << member + assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count + end + + class ClubWithCallbacks < ActiveRecord::Base + self.table_name = 'clubs' + after_create :add_a_member + + has_many :memberships, inverse_of: :club, foreign_key: :club_id + has_many :members, through: :memberships + + def add_a_member + members << Member.last + end + end + + def test_has_many_with_callback_before_association + Member.create! + club = ClubWithCallbacks.create! + + assert_equal 1, club.reload.memberships.count + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index a2725441b3..089cb0a3a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_creating_association_sets_both_parent_ids_for_new + member = Member.new(name: 'Sean Griffin') + club = Club.new(name: 'Da Club') + + member.club = club + + member.save! + + assert member.id + assert club.id + assert_equal member.id, member.current_membership.member_id + assert_equal club.id, member.current_membership.club_id + end + def test_replace_target_record new_club = Club.create(:name => "Marx Bros") @member.club = new_club diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index a9efa6d86a..07cf65a760 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -117,4 +117,23 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert_equal [author], Author.where(id: author).joins(:special_categorizations) end + + test "the default scope of the target is correctly aliased when joining associations" do + author = Author.create! name: "Jon" + author.categories.create! name: 'Not Special' + author.special_categories.create! name: 'Special' + + categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a + assert_equal 2, categories.size + end + + test "the correct records are loaded when including an aliased association" do + author = Author.create! name: "Jon" + author.categories.create! name: 'Not Special' + author.special_categories.create! name: 'Special' + + categories = author.categories.eager_load(:special_categorizations).order(:name).to_a + assert_equal 0, categories.first.special_categorizations.size + assert_equal 1, categories.second.special_categorizations.size + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 893030345f..60df4e14dd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_respond_to club_reflection, :has_inverse? assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end + + def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name + man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) + face_reflection = Face.reflect_on_association(:man) + + assert_respond_to face_reflection, :has_inverse? + assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse" + + assert_respond_to man_reflection, :has_inverse? + assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically" + end end class InverseAssociationTests < ActiveRecord::TestCase @@ -333,7 +344,7 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_within_create_block_of_new_child man = Man.first - interest = man.interests.build do |i| + interest = man.interests.create do |i| assert i.man.equal?(man), "Man of child should be the same instance as a parent" end assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index aabeea025f..cace7ba142 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_polymorphic_with_counter_cache - assert_equal 1, posts(:welcome)[:taggings_count] + assert_equal 1, posts(:welcome)[:tags_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) - assert_equal 2, posts(:welcome, :reload)[:taggings_count] + assert_equal 2, posts(:welcome, :reload)[:tags_count] tagging.destroy - assert_equal 1, posts(:welcome, :reload)[:taggings_count] + assert_equal 1, posts(:welcome, :reload)[:tags_count] end def test_unavailable_through_reflection @@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 1, post_thinking.tags.size) + assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags(true).size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') @@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 2, post_thinking.tags.size) + assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags(true).size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } @@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 4, post_thinking.tags.size) + assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags(true).size) # Raises if the wrong reflection name is used to set the Edge belongs_to @@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_associate_when_deleting_from_has_many_through count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) assert_equal(count, post_thinking.taggings(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort doomed = Tag.create!(:name => 'doomed') doomed2 = Tag.create!(:name => 'doomed2') quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags(true).size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 8ef351cda8..31b68c940e 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [mustache], members.first.nested_sponsors end end @@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + ActiveRecord::Base.connection.table_alias_length # preheat cache members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb new file mode 100644 index 0000000000..a6934a056e --- /dev/null +++ b/activerecord/test/cases/associations/required_test.rb @@ -0,0 +1,82 @@ +require "cases/helper" + +class RequiredAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Parent < ActiveRecord::Base + end + + class Child < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :parents, force: true + @connection.create_table :children, force: true do |t| + t.belongs_to :parent + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS parents") + @connection.execute("DROP TABLE IF EXISTS children") + end + + test "belongs_to associations are not required by default" do + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + assert model.new.save + assert model.new(parent: Parent.new).save + end + + test "required belongs_to associations have presence validated" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent can't be blank"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + end + + test "has_one associations are not required by default" do + model = subclass_of(Parent) do + has_one :child, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + assert model.new.save + assert model.new(child: Child.new).save + end + + test "required has_one associations have presence validated" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.new + assert_not record.save + assert_equal ["Child can't be blank"], record.errors.full_messages + + record.child = Child.new + assert record.save + end + + private + + def subclass_of(klass, &block) + subclass = Class.new(klass, &block) + def subclass.name + superclass.name + end + subclass + end +end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb new file mode 100644 index 0000000000..cbc2c4e5d7 --- /dev/null +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -0,0 +1,124 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeDecoratorsTest < ActiveRecord::TestCase + class Model < ActiveRecord::Base + self.table_name = 'attribute_decorators_model' + end + + class StringDecorator < SimpleDelegator + def initialize(delegate, decoration = "decorated!") + @decoration = decoration + super(delegate) + end + + def type_cast_from_user(value) + "#{super} #{@decoration}" + end + + alias type_cast_from_database type_cast_from_user + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :attribute_decorators_model, force: true do |t| + t.string :a_string + end + end + + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model' + Model.attribute_type_decorations.clear + Model.reset_column_information + end + + test "attributes can be decorated" do + model = Model.new(a_string: 'Hello') + assert_equal 'Hello', model.a_string + + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decoration does not eagerly load existing columns" do + assert_no_queries do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + end + end + + test "undecorated columns are not touched" do + Model.attribute :another_string, Type::String.new, default: 'something or other' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + assert_equal 'something or other', Model.new.another_string + end + + test "decorators can be chained" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated!', model.a_string + end + + test "decoration of the same type multiple times is idempotent" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decorations occur in order of declaration" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) do |type| + StringDecorator.new(type, 'decorated again!') + end + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated again!', model.a_string + end + + test "decorating attributes does not modify parent classes" do + Model.attribute :another_string, Type::String.new, default: 'whatever' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + child_class = Class.new(Model) + child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) } + child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + child = child_class.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated!', model.a_string + assert_equal 'whatever', model.another_string + assert_equal 'Hello! decorated! decorated!', child.a_string + assert_equal 'whatever decorated!', child.another_string + end + + class Multiplier < SimpleDelegator + def type_cast_from_user(value) + return if value.nil? + value * 2 + end + alias type_cast_from_database type_cast_from_user + end + + test "decorating with a proc" do + Model.attribute :an_int, Type::Integer.new + type_is_integer = proc { |_, type| type.type == :integer } + Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| + Multiplier.new(type) + end + + model = Model.new(a_string: 'whatever', an_int: 1) + + assert_equal 'whatever', model.a_string + assert_equal 2, model.an_int + end + end +end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index c0659fddef..4741ee8799 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,6 +12,7 @@ module ActiveRecord @klass = Class.new do def self.superclass; Base; end def self.base_class; self; end + def self.decorate_matching_attribute_types(*); end include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb deleted file mode 100644 index 75de773961..0000000000 --- a/activerecord/test/cases/attribute_methods/serialization_test.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module AttributeMethods - class SerializationTest < ActiveSupport::TestCase - class FakeColumn < Struct.new(:name) - def type; :integer; end - def type_cast(s); "#{s}!"; end - end - - class NullCoder - def load(v); v; end - end - - def test_type_cast_serialized_value - value = Serialization::Attribute.new(NullCoder.new, "Hello world", :serialized) - type = Serialization::Type.new(FakeColumn.new) - assert_equal "Hello world!", type.type_cast(value) - end - - def test_type_cast_unserialized_value - value = Serialization::Attribute.new(nil, "Hello world", :unserialized) - type = Serialization::Type.new(FakeColumn.new) - type.type_cast(value) - assert_equal "Hello world", type.type_cast(value) - end - end - end -end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 6c581a432f..ab67cf4085 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -22,7 +22,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase @target.table_name = 'topics' end - def teardown + teardown do ActiveRecord::Base.send(:attribute_method_matchers).clear ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end @@ -143,7 +143,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object - topic = Topic.allocate + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'topics' + end + + topic = klass.allocate assert !topic.respond_to?("nothingness") assert !topic.respond_to?(:nothingness) assert_respond_to topic, "title" @@ -253,6 +257,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end + def test_attributes_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + + assert_equal klass.column_names, klass.new.attributes.keys + assert_not klass.new.attributes.key?('id') + end + def test_hashes_not_mangled new_topic = { :title => "New Topic" } new_topic_values = { :title => "AnotherTopic" } @@ -288,10 +301,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_read_attribute topic = Topic.new topic.title = "Don't change the topic" - assert_equal "Don't change the topic", topic.send(:read_attribute, "title") + assert_equal "Don't change the topic", topic.read_attribute("title") assert_equal "Don't change the topic", topic["title"] - assert_equal "Don't change the topic", topic.send(:read_attribute, :title) + assert_equal "Don't change the topic", topic.read_attribute(:title) assert_equal "Don't change the topic", topic[:title] end @@ -299,6 +312,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase computer = Computer.select('id').first assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] } assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] } + assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' } + assert_nothing_raised { computer[:developer] = 'Hello!' } end def test_read_attribute_when_false @@ -358,10 +373,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase super(attr_name).upcase end - assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, "title") + assert_equal "STOP CHANGING THE TOPIC", topic.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.read_attribute(:title) assert_equal "STOP CHANGING THE TOPIC", topic[:title] end @@ -449,10 +464,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix + topic = @target.new(:title => 'Budget') meth = "title#{suffix}" assert topic.respond_to?(meth) @@ -463,10 +478,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + topic = @target.new(:title => 'Budget') meth = "#{prefix}title#{suffix}" assert topic.respond_to?(meth) @@ -515,44 +530,36 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_only_time_related_columns_are_meant_to_be_cached_by_default - expected = %w(datetime timestamp time date).sort - assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort - end + def test_deprecated_cache_attributes + assert_deprecated do + Topic.cache_attributes :replies_count + end - def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default - default_attributes = Topic.cached_attributes - Topic.cache_attributes :replies_count - expected = default_attributes + ["replies_count"] - assert_equal expected.sort, Topic.cached_attributes.sort - Topic.instance_variable_set "@cached_attributes", nil - end + assert_deprecated do + Topic.cached_attributes + end - def test_cacheable_columns_are_actually_cached - assert_equal cached_columns.sort, Topic.cached_attributes.sort + assert_deprecated do + Topic.cache_attribute? :replies_count + end end - def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else - t = topics(:first) - cache = t.instance_variable_get "@attributes_cache" + def test_converted_values_are_returned_after_assignment + developer = Developer.new(name: 1337, salary: "50000") - assert_not_nil cache - assert cache.empty? + assert_equal "50000", developer.salary_before_type_cast + assert_equal 1337, developer.name_before_type_cast - all_columns = Topic.columns.map(&:name) - uncached_columns = all_columns - cached_columns + assert_equal 50000, developer.salary + assert_equal "1337", developer.name - all_columns.each do |attr_name| - attribute_gets_cached = Topic.cache_attribute?(attr_name) - val = t.send attr_name unless attr_name == "type" - if attribute_gets_cached - assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name] - else - assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name) - end - end + developer.save! + + assert_equal "50000", developer.salary_before_type_cast + assert_equal 1337, developer.name_before_type_cast + + assert_equal 50000, developer.salary + assert_equal "1337", developer.name end def test_write_nil_to_time_attributes @@ -728,19 +735,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert "unknown attribute: hello", error.message end - def test_read_attribute_overwrites_private_method_not_considered_implemented - # simulate a model with a db column that shares its name an inherited - # private method (e.g. Object#system) - # - Object.class_eval do - private - def title; "private!"; end + def test_methods_override_in_multi_level_subclass + klass = Class.new(Developer) do + def name + "dev:#{read_attribute(:name)}" + end end - assert !@target.instance_method_already_implemented?(:title) - topic = @target.new - assert_nil topic.title - Object.send(:undef_method, :title) # remove test method from object + 2.times { klass = Class.new klass } + dev = klass.new(name: 'arthurnn') + dev.save! + assert_equal 'dev:arthurnn', dev.reload.name + end + + def test_global_methods_are_overwritten + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'computers' + end + + assert !klass.instance_method_already_implemented?(:system) + computer = klass.new + assert_nil computer.system + end + + def test_global_methods_are_overwritte_when_subclassing + klass = Class.new(ActiveRecord::Base) { self.abstract_class = true } + + subklass = Class.new(klass) do + self.table_name = 'computers' + end + + assert !klass.instance_method_already_implemented?(:system) + assert !subklass.instance_method_already_implemented?(:system) + computer = subklass.new + assert_nil computer.system end def test_instance_method_should_be_defined_on_the_base_class @@ -804,6 +832,49 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal !real_topic.title?, klass.find(real_topic.id).title? end + def test_calling_super_when_parent_does_not_define_method_raises_error + klass = new_topic_like_ar_class do + def some_method_that_is_not_on_super + super + end + end + + assert_raise(NoMethodError) do + klass.new.some_method_that_is_not_on_super + end + end + + def test_attribute_method? + assert @target.attribute_method?(:title) + assert @target.attribute_method?(:title=) + assert_not @target.attribute_method?(:wibble) + end + + def test_attribute_method_returns_false_if_table_does_not_exist + @target.table_name = 'wibble' + assert_not @target.attribute_method?(:title) + end + + def test_attribute_names_on_new_record + model = @target.new + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_on_queried_record + model = @target.last! + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_with_custom_select + model = @target.select('id').last! + + assert_equal ['id'], model.attribute_names + # Sanity check, make sure other columns exist + assert_not_equal ['id'], @target.column_names + end + private def new_topic_like_ar_class(&block) @@ -817,7 +888,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def cached_columns - Topic.columns.map(&:name) - Topic.serialized_attributes.keys + Topic.columns.map(&:name) end def time_related_columns_on_topic diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb new file mode 100644 index 0000000000..dc20c3c676 --- /dev/null +++ b/activerecord/test/cases/attribute_set_test.rb @@ -0,0 +1,165 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeSetTest < ActiveRecord::TestCase + test "building a new set from raw attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes[:foo].value + assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name + end + + test "building with custom types" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new }) + + assert_equal 3.3, attributes[:foo].value + assert_equal 4, attributes[:bar].value + end + + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: '3.3') + + assert_equal '3.3', attributes[:foo].value_before_type_cast + assert_equal nil, attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name + end + + test "duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal 'foo', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "freezing cloned set does not freeze original" do + attributes = AttributeSet.new({}) + clone = attributes.clone + + clone.freeze + + assert clone.frozen? + assert_not attributes.frozen? + end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert attributes.key?(:foo) + assert_not attributes.key?(:bar) + end + + test "unknown attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.key?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end + end +end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb new file mode 100644 index 0000000000..91f6aee931 --- /dev/null +++ b/activerecord/test/cases/attribute_test.rb @@ -0,0 +1,142 @@ +require 'cases/helper' +require 'minitest/mock' + +module ActiveRecord + class AttributeTest < ActiveRecord::TestCase + setup do + @type = Minitest::Mock.new + end + + teardown do + assert @type.verify + end + + test "from_database + read type casts from database" do + @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from database', type_cast_value + end + + test "from_user + read type casts from user" do + @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + attribute = Attribute.from_user(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from user', type_cast_value + end + + test "reading memoizes the value" do + @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_value = attribute.value + second_read = attribute.value + + assert_equal 'from the database', type_cast_value + assert_same type_cast_value, second_read + end + + test "reading memoizes falsy values" do + @type.expect(:type_cast_from_database, false, ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + attribute.value + attribute.value + end + + test "read_before_typecast returns the given value" do + attribute = Attribute.from_database(nil, 'raw value', @type) + + raw_value = attribute.value_before_type_cast + + assert_equal 'raw value', raw_value + end + + test "from_database + read_for_database type casts to and from database" do + @type.expect(:type_cast_from_database, 'read from database', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "from_user + read_for_database type casts from the user to the database" do + @type.expect(:type_cast_from_user, 'read from user', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + attribute = Attribute.from_user(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "duping dups the value" do + @type.expect(:type_cast_from_database, 'type cast', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + value_from_orig = attribute.value + value_from_clone = attribute.dup.value + value_from_orig << ' foo' + + assert_equal 'type cast foo', value_from_orig + assert_equal 'type cast', value_from_clone + end + + test "duping does not dup the value if it is not dupable" do + @type.expect(:type_cast_from_database, false, ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + assert_same attribute.value, attribute.dup.value + end + + test "duping does not eagerly type cast if we have not yet type cast" do + attribute = Attribute.from_database(nil, 'a value', @type) + attribute.dup + end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + end +end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb new file mode 100644 index 0000000000..79ef0502cb --- /dev/null +++ b/activerecord/test/cases/attributes_test.rb @@ -0,0 +1,111 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + attribute :overloaded_float, Type::Integer.new + attribute :overloaded_string_with_limit, Type::String.new(limit: 50) + attribute :non_existent_decimal, Type::Decimal.new + attribute :string_with_default, Type::String.new, default: 'the overloaded default' +end + +class ChildOfOverloadedType < OverloadedType +end + +class GrandchildOfOverloadedType < ChildOfOverloadedType + attribute :overloaded_float, Type::Float.new +end + +class UnoverloadedType < ActiveRecord::Base + self.table_name = 'overloaded_types' +end + +module ActiveRecord + class CustomPropertiesTest < ActiveRecord::TestCase + def test_overloading_types + data = OverloadedType.new + + data.overloaded_float = "1.1" + data.unoverloaded_float = "1.1" + + assert_equal 1, data.overloaded_float + assert_equal 1.1, data.unoverloaded_float + end + + def test_overloaded_properties_save + data = OverloadedType.new + + data.overloaded_float = "2.2" + data.save! + data.reload + + assert_equal 2, data.overloaded_float + assert_kind_of Fixnum, OverloadedType.last.overloaded_float + assert_equal 2.0, UnoverloadedType.last.overloaded_float + assert_kind_of Float, UnoverloadedType.last.overloaded_float + end + + def test_properties_assigned_in_constructor + data = OverloadedType.new(overloaded_float: '3.3') + + assert_equal 3, data.overloaded_float + end + + def test_overloaded_properties_with_limit + assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + end + + def test_nonexistent_attribute + data = OverloadedType.new(non_existent_decimal: 1) + + assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_raise ActiveRecord::UnknownAttributeError do + UnoverloadedType.new(non_existent_decimal: 1) + end + end + + def test_changing_defaults + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_equal 'the overloaded default', data.string_with_default + assert_equal 'the original default', unoverloaded_data.string_with_default + end + + def test_children_inherit_custom_properties + data = ChildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4, data.overloaded_float + end + + def test_children_can_override_parents + data = GrandchildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4.4, data.overloaded_float + end + + def test_overloading_properties_does_not_change_column_order + column_names = OverloadedType.column_names + assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names + end + + def test_caches_are_cleared + klass = Class.new(OverloadedType) + + assert_equal 6, klass.columns.length + assert_not klass.columns_hash.key?('wibble') + assert_equal 6, klass.column_types.length + assert_equal 6, klass.column_defaults.length + assert_not klass.column_names.include?('wibble') + assert_equal 5, klass.content_columns.length + + klass.attribute :wibble, Type::Value.new + + assert_equal 7, klass.columns.length + assert klass.columns_hash.key?('wibble') + assert_equal 7, klass.column_types.length + assert_equal 7, klass.column_defaults.length + assert klass.column_names.include?('wibble') + assert_equal 6, klass.content_columns.length + end + end +end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index c55dd685a1..09892d50ba 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -21,6 +21,31 @@ require 'models/electron' require 'models/molecule' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase + def test_autosave_validation + person = Class.new(ActiveRecord::Base) { + self.table_name = 'people' + validate :should_be_cool, :on => :create + def self.name; 'Person'; end + + private + + def should_be_cool + unless self.first_name == 'cool' + errors.add :first_name, "not cool" + end + end + } + reference = Class.new(ActiveRecord::Base) { + self.table_name = "references" + def self.name; 'Reference'; end + belongs_to :person, autosave: true, class: person + } + + u = person.create!(first_name: 'cool') + u.update_attributes!(first_name: 'nah') # still valid because validation only applies on 'create' + assert reference.create!(person: u).persisted? + end + def test_should_not_add_the_same_callbacks_multiple_times_for_has_one assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship end @@ -39,10 +64,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private - def base - ActiveRecord::Base - end - def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) reflection = model.reflect_on_association(association_name) assert_no_difference "callbacks_for_model(#{model.name}).length" do @@ -51,9 +72,9 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase end def callbacks_for_model(model) - model.instance_variables.grep(/_callbacks$/).map do |ivar| + model.instance_variables.grep(/_callbacks$/).flat_map do |ivar| model.instance_variable_get(ivar) - end.flatten + end end end @@ -597,15 +618,15 @@ end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false - def setup - super + setup do @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end - def teardown + teardown do # We are running without transactional fixtures and need to cleanup. Bird.delete_all + Parrot.delete_all @ship.delete @pirate.delete end @@ -662,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end + @ship.pirate.catchphrase = "Changed Catchphrase" + assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship end + def test_should_save_changed_has_one_changed_object_if_child_is_saved + @pirate.ship.name = "NewName" + assert @pirate.save + assert_equal "NewName", @pirate.ship.reload.name + end + + def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved + @pirate.ship.expects(:save).never + assert @pirate.save + end + # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal assert !@ship.pirate.marked_for_destruction? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8a0b0b9589..4c0b0c868a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_columns_should_obey_set_primary_key - pk = Subscriber.columns.find { |x| x.name == 'nick' } - assert pk.primary, 'nick should be primary key' + pk = Subscriber.columns_hash[Subscriber.primary_key] + assert_equal 'nick', pk.name, 'nick should be primary key' end def test_primary_key_with_no_id @@ -160,19 +160,11 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_date_objects - if current_adapter?(:SybaseAdapter) - # Sybase ctlib does not (yet?) support the date type; use datetime instead. - assert_kind_of( - Time, Topic.find(1).last_read, - "The last_read attribute should be of the Time class" - ) - else - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_kind_of( - Date, Topic.find(1).last_read, - "The last_read attribute should be of the Date class" - ) - end + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) + assert_kind_of( + Date, Topic.find(1).last_read, + "The last_read attribute should be of the Date class" + ) end def test_previously_changed @@ -212,7 +204,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56? assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -480,8 +472,8 @@ class BasicsTest < ActiveRecord::TestCase end end - # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + unless current_adapter?(:OracleAdapter) def test_utc_as_time_zone with_timezone_config default: :utc do attributes = { "bonus_time" => "5:42:00AM" } @@ -515,12 +507,7 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.find(topic.id) assert_nil topic.last_read - # Sybase adapter does not allow nulls in boolean columns - if current_adapter?(:SybaseAdapter) - assert topic.approved == false - else - assert_nil topic.approved - end + assert_nil topic.approved end def test_equality @@ -531,8 +518,13 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find('1-meowmeow'), Topic.find(1) end + def test_find_by_slug_with_array + assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new + assert_equal false, Topic.new == Topic.new end def test_equality_of_destroyed_records @@ -544,6 +536,47 @@ class BasicsTest < ActiveRecord::TestCase assert_equal topic_2, topic_1 end + def test_equality_with_blank_ids + one = Subscriber.new(:id => '') + two = Subscriber.new(:id => '') + assert_equal one, two + end + + def test_equality_of_relation_and_collection_proxy + car = Car.create! + car.bulbs.build + car.save + + assert car.bulbs == Bulb.where(car_id: car.id), 'CollectionProxy should be comparable with Relation' + assert Bulb.where(car_id: car.id) == car.bulbs, 'Relation should be comparable with CollectionProxy' + end + + def test_equality_of_relation_and_array + car = Car.create! + car.bulbs.build + car.save + + assert Bulb.where(car_id: car.id) == car.bulbs.to_a, 'Relation should be comparable with Array' + end + + def test_equality_of_relation_and_association_relation + car = Car.create! + car.bulbs.build + car.save + + assert_equal Bulb.where(car_id: car.id), car.bulbs.includes(:car), 'Relation should be comparable with AssociationRelation' + assert_equal car.bulbs.includes(:car), Bulb.where(car_id: car.id), 'AssociationRelation should be comparable with Relation' + end + + def test_equality_of_collection_proxy_and_association_relation + car = Car.create! + car.bulbs.build + car.save + + assert_equal car.bulbs, car.bulbs.includes(:car), 'CollectionProxy should be comparable with AssociationRelation' + assert_equal car.bulbs.includes(:car), car.bulbs, 'AssociationRelation should be comparable with CollectionProxy' + end + def test_hashing assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end @@ -578,12 +611,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal nil, Topic.find_by_id(topic.id) end - def test_blank_ids - one = Subscriber.new(:id => '') - two = Subscriber.new(:id => '') - assert_equal one, two - end - def test_comparison_with_different_objects topic = Topic.create category = Category.create(:name => "comparison") @@ -649,8 +676,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_attributes_on_dummy_time - # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + return true if current_adapter?(:OracleAdapter) with_timezone_config default: :local do attributes = { @@ -663,8 +690,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_attributes_on_dummy_time_with_invalid_time - # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + return true if current_adapter?(:OracleAdapter) attributes = { "bonus_time" => "not a time" @@ -751,8 +778,14 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("c", duped_topic.title) end + DeveloperSalary = Struct.new(:amount) def test_dup_with_aggregate_of_same_name_as_attribute - dev = DeveloperWithAggregate.find(1) + developer_with_aggregate = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)] + end + + dev = developer_with_aggregate.find(1) assert_kind_of DeveloperSalary, dev.salary dup = nil @@ -951,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions @@ -1310,14 +1347,32 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) assert_raises NoMethodError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end end + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) assert_raises ArgumentError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end @@ -1451,15 +1506,14 @@ class BasicsTest < ActiveRecord::TestCase attrs = topic.attributes.dup attrs.delete 'id' - typecast = Class.new { + typecast = Class.new(ActiveRecord::Type::Value) { def type_cast value "t.lo" end } types = { 'author_name' => typecast.new } - topic = Topic.allocate.init_with 'attributes' => attrs, - 'column_types' => types + topic = Topic.instantiate(attrs, types) assert_equal 't.lo', topic.author_name end @@ -1560,4 +1614,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal after_handler, new_handler assert_equal orig_handler, klass.connection_handler end + + # Note: This is a performance optimization for Array#uniq and Hash#[] with + # AR::Base objects. If the future has made this irrelevant, feel free to + # delete this. + test "records without an id have unique hashes" do + assert_not_equal Post.new.hash, Post.new.hash + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 9a486cf8b8..ccf2be369d 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -2,9 +2,9 @@ require "cases/helper" # Without using prepared statements, it makes no sense to test -# BLOB data with DB2 or Firebird, because the length of a statement +# BLOB data with DB2, because the length of a statement # is limited to 32KB. -unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) +unless current_adapter?(:DB2Adapter) require 'models/binary' class BinaryTest < ActiveRecord::TestCase @@ -21,7 +21,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) name = binary.name - # Mysql adapter doesn't properly encode things, so we have to do it + # MySQL adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) name.force_encoding(Encoding::UTF_8) end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 291751c435..0bc7ee6d64 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -20,13 +20,13 @@ module ActiveRecord def setup super @connection = ActiveRecord::Base.connection - @listener = LogListener.new - @pk = Topic.columns.find { |c| c.primary } - ActiveSupport::Notifications.subscribe('sql.active_record', @listener) + @subscriber = LogListener.new + @pk = Topic.columns_hash[Topic.primary_key] + @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end - def teardown - ActiveSupport::Notifications.unsubscribe(@listener) + teardown do + ActiveSupport::Notifications.unsubscribe(@subscription) end if ActiveRecord::Base.connection.supports_statement_cache? @@ -37,7 +37,7 @@ module ActiveRecord @connection.exec_query(sql, 'SQL', binds) - message = @listener.calls.find { |args| args[4][:sql] == sql } + message = @subscriber.calls.find { |args| args[4][:sql] == sql } assert_equal binds, message[4][:binds] end @@ -48,24 +48,22 @@ module ActiveRecord @connection.exec_query(sql, 'SQL', binds) - message = @listener.calls.find { |args| args[4][:sql] == sql } + message = @subscriber.calls.find { |args| args[4][:sql] == sql } assert_equal [[@pk, 3]], message[4][:binds] end def test_find_one_uses_binds Topic.find(1) binds = [[@pk, 1]] - message = @listener.calls.find { |args| args[4][:binds] == binds } + message = @subscriber.calls.find { |args| args[4][:binds] == binds } assert message, 'expected a message with binds' end def test_logs_bind_vars - pk = Topic.columns.find { |x| x.primary } - payload = { :name => 'SQL', :sql => 'select * from topics where id = ?', - :binds => [[pk, 10]] + :binds => [[@pk, 10]] } event = ActiveSupport::Notifications::Event.new( 'foo', @@ -87,7 +85,7 @@ module ActiveRecord }.new logger.sql event - assert_match([[pk.name, 10]].inspect, logger.debugs.first) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index db999f90ab..319ea9260a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -15,6 +15,10 @@ Company.has_many :accounts class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase @@ -49,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_nil NumericData.average(:bank_balance) end - def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal - assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg') - assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg') - end - def test_should_get_maximum_of_field assert_equal 60, Account.maximum(:credit_limit) end @@ -387,6 +386,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Account.count(1, 2, 3) } end + def test_count_with_order + assert_equal 6, Account.order(:credit_limit).count + end + + def test_count_with_reverse_order + assert_equal 6, Account.order(:credit_limit).reverse_order.count + end + + def test_count_with_where_and_order + assert_equal 1, Account.where(firm_name: '37signals').count + assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).count + assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count + end + def test_should_sum_expression # Oracle adapter returns floating point value 636.0 after SUM if current_adapter?(:OracleAdapter) @@ -592,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [1,2,3,4,5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end + + def test_pluck_columns_with_same_name + expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]] + actual = Topic.joins(:replies) + .pluck('topics.title', 'replies_topics.title') + assert_equal expected, actual + end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index c7b64f29c3..bcfd66b4bf 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -11,30 +11,10 @@ module ActiveRecord @viz = @adapter.schema_creation end - def test_can_set_coder - column = Column.new("title", nil, "varchar(20)") - column.coder = YAML - assert_equal YAML, column.coder - end - - def test_encoded? - column = Column.new("title", nil, "varchar(20)") - assert !column.encoded? - - column.coder = YAML - assert column.encoded? - end - - def test_type_case_coded_column - column = Column.new("title", nil, "varchar(20)") - column.coder = YAML - assert_equal "hello", column.type_cast("--- hello") - end - # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, "varchar(20)") + column = Column.new("title", nil, Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -42,7 +22,7 @@ module ActiveRecord end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", "varchar(20)") + column = Column.new("title", "Hello", Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -50,7 +30,7 @@ module ActiveRecord end def test_should_specify_not_null_if_null_option_is_false - column = Column.new("title", "Hello", "varchar(20)", false) + column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -59,81 +39,81 @@ module ActiveRecord if current_adapter?(:MysqlAdapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)") + binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)") assert_equal "a", binary_column.default - varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)") + varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "a", "blob") + MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob") end assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "Hello", "text") + MysqlAdapter::Column.new("title", "Hello", Type::Text.new) end - text_column = MysqlAdapter::Column.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) assert_equal nil, text_column.default - not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false) + not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = MysqlAdapter::Column.new("title", nil, "blob") + blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob") assert !blob_column.has_default? - text_column = MysqlAdapter::Column.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) assert !text_column.has_default? end end if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)") + binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)") assert_equal "a", binary_column.default - varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)") + varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "a", "blob") + Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob") end assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "Hello", "text") + Mysql2Adapter::Column.new("title", "Hello", Type::Text.new) end - text_column = Mysql2Adapter::Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) assert_equal nil, text_column.default - not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false) + not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = Mysql2Adapter::Column.new("title", nil, "blob") + blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob") assert !blob_column.has_default? - text_column = Mysql2Adapter::Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) assert !text_column.has_default? end end if current_adapter?(:PostgreSQLAdapter) def test_bigint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Identity.new + oid = PostgreSQLAdapter::OID::Integer.new bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint") assert_equal :integer, bigint_column.type end def test_smallint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Identity.new + oid = PostgreSQLAdapter::OID::Integer.new smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") assert_equal :integer, smallint_column.type end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb deleted file mode 100644 index 2a6d8cc2ab..0000000000 --- a/activerecord/test/cases/column_test.rb +++ /dev/null @@ -1,123 +0,0 @@ -require "cases/helper" -require 'models/company' - -module ActiveRecord - module ConnectionAdapters - class ColumnTest < ActiveRecord::TestCase - def test_type_cast_boolean - column = Column.new("field", nil, "boolean") - assert column.type_cast('').nil? - assert column.type_cast(nil).nil? - - 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') - - # explicitly check for false vs nil - assert_equal false, column.type_cast(false) - assert_equal false, column.type_cast(0) - assert_equal false, column.type_cast('0') - assert_equal false, column.type_cast('f') - assert_equal false, column.type_cast('F') - assert_equal false, column.type_cast('false') - assert_equal false, column.type_cast('FALSE') - assert_equal false, column.type_cast('off') - assert_equal false, column.type_cast('OFF') - assert_equal false, column.type_cast(' ') - assert_equal false, column.type_cast("\u3000\r\n") - assert_equal false, column.type_cast("\u0000") - assert_equal false, column.type_cast('SOMETHING RANDOM') - end - - def test_type_cast_integer - column = Column.new("field", nil, "integer") - assert_equal 1, column.type_cast(1) - assert_equal 1, column.type_cast('1') - assert_equal 1, column.type_cast('1ignore') - assert_equal 0, column.type_cast('bad1') - assert_equal 0, column.type_cast('bad') - assert_equal 1, column.type_cast(1.7) - assert_equal 0, column.type_cast(false) - assert_equal 1, column.type_cast(true) - assert_nil column.type_cast(nil) - end - - def test_type_cast_non_integer_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast([1,2]) - assert_nil column.type_cast({1 => 2}) - assert_nil column.type_cast((1..2)) - end - - def test_type_cast_activerecord_to_integer - column = Column.new("field", nil, "integer") - firm = Firm.create(:name => 'Apple') - assert_nil column.type_cast(firm) - end - - def test_type_cast_object_without_to_i_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast(Float::NAN) - assert_nil column.type_cast(1.0/0.0) - end - - def test_type_cast_time - column = Column.new("field", nil, "time") - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, column.type_cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") - end - end - - def test_type_cast_date - column = Column.new("field", nil, "date") - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, column.type_cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - column = Column.new("field", nil, "integer") - assert_equal 1800, column.type_cast(30.minutes) - assert_equal 7200, column.type_cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :local].each do |zone| - with_timezone_config default: zone do - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - 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 deleted file mode 100644 index eb2fe5639b..0000000000 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - class ConnectionPool - def insert_connection_for_test!(c) - synchronize do - @connections << c - @available.add c - end - end - end - - class AbstractAdapterTest < ActiveRecord::TestCase - attr_reader :adapter - - def setup - @adapter = AbstractAdapter.new nil, nil - end - - def test_in_use? - assert_not 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_not adapter.lease, 'should not lease adapter' - end - - def test_last_use - assert_not 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_not adapter.in_use?, 'adapter is in use' - end - - def test_close - pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) - pool.insert_connection_for_test! 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_not adapter.in_use? - - assert_equal adapter, pool.connection - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb new file mode 100644 index 0000000000..662e19f35e --- /dev/null +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class AdapterLeasingTest < ActiveRecord::TestCase + class Pool < ConnectionPool + def insert_connection_for_test!(c) + synchronize do + @connections << c + @available.add c + end + end + end + + def setup + @adapter = AbstractAdapter.new nil, nil + end + + def test_in_use? + assert_not @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_not @adapter.lease, 'should not lease adapter' + end + + def test_expire_mutates_in_use + assert @adapter.lease, 'lease adapter' + assert @adapter.in_use?, 'adapter is in use' + @adapter.expire + assert_not @adapter.in_use?, 'adapter is in use' + end + + def test_close + pool = Pool.new(ConnectionSpecification.new({}, nil)) + pool.insert_connection_for_test! @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_not @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 318cc5a32c..3e33b30144 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -2,133 +2,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters - - class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase - - def klass - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig - end - - def setup - @previous_database_url = ENV.delete("DATABASE_URL") - end - - def teardown - ENV["DATABASE_URL"] = @previous_database_url - end - - def test_string_connection - config = { "production" => "postgres://localhost/foo" } - actual = klass.new(config).resolve - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_url_sub_key - config = { "production" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_hash - config = { "production" => { "adapter" => "postgres", "database" => "foo" } } - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank - config = {} - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {} - actual = klass.new(config).resolve - expected = { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" } - assert_equal expected, actual["production"] - assert_equal expected, actual["development"] - assert_equal expected, actual["test"] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] - end - - def test_sting_with_database_url - ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" - - config = { "production" => "postgres://localhost/foo" } - actual = klass.new(config).resolve - - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_url_sub_key_with_database_url - ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" - - config = { "production" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_merge_no_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"production" => { "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - - def test_merge_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"production" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "production" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - end - class ConnectionHandlerTest < ActiveRecord::TestCase def setup @klass = Class.new(Base) { def self.name; 'klass'; end } diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb new file mode 100644 index 0000000000..e1b2804a18 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -0,0 +1,204 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase + def setup + @previous_database_url = ENV.delete("DATABASE_URL") + end + + teardown do + ENV["DATABASE_URL"] = @previous_database_url + end + + def resolve_config(config) + ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + end + + def resolve_spec(spec, config) + ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + end + + def test_resolver_with_database_uri_and_current_env_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_and_current_env_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = assert_deprecated { resolve_spec("default_env", config) } + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_known_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:production, config) + expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_unknown_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_raises AdapterNotSpecified do + resolve_spec(:production, config) + end + end + + def test_resolver_with_database_uri_and_unknown_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_deprecated do + assert_raises AdapterNotSpecified do + resolve_spec("production", config) + end + end + end + + def test_resolver_with_database_uri_and_supplied_url + ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" + config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } + actual = resolve_spec("postgres://localhost/foo", config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_jdbc_url + config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_environment_does_not_exist_in_config_url_does_exist + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_config(config) + expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expect_prod, actual["default_env"] + end + + def test_url_with_hyphenated_scheme + ENV['DATABASE_URL'] = "ibm-db://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_string_connection + config = { "default_env" => "postgres://localhost/foo" } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_url_sub_key + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_hash + config = { "production" => { "adapter" => "postgres", "database" => "foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank + config = {} + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + assert_equal expected, actual["default_env"] + assert_equal nil, actual["production"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_database_url_with_ipv6_host_and_port + ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "::1", + "port" => 5454 } + assert_equal expected, actual["default_env"] + end + + def test_url_sub_key_with_database_url + ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" + + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_merge_no_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + + def test_merge_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb new file mode 100644 index 0000000000..d4d67487db --- /dev/null +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -0,0 +1,61 @@ +require "cases/helper" + +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +module ActiveRecord + module ConnectionAdapters + class MysqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_boolean_types + emulate_booleans(true) do + assert_lookup_type :boolean, 'tinyint(1)' + assert_lookup_type :boolean, 'TINYINT(1)' + end + end + + def test_string_types + assert_lookup_type :string, "enum('one', 'two', 'three')" + assert_lookup_type :string, "ENUM('one', 'two', 'three')" + assert_lookup_type :string, "set('one', 'two', 'three')" + assert_lookup_type :string, "SET('one', 'two', 'three')" + end + + def test_binary_types + assert_lookup_type :binary, 'bit' + assert_lookup_type :binary, 'BIT' + end + + def test_integer_types + emulate_booleans(false) do + assert_lookup_type :integer, 'tinyint(1)' + assert_lookup_type :integer, 'TINYINT(1)' + assert_lookup_type :integer, 'year' + assert_lookup_type :integer, 'YEAR' + end + end + + private + + def assert_lookup_type(type, lookup) + cast_type = @connection.type_map.lookup(lookup) + assert_equal type, cast_type.type + end + + def emulate_booleans(value) + old_emulate_booleans = @connection.emulate_booleans + change_emulate_booleans(value) + yield + ensure + change_emulate_booleans(old_emulate_booleans) + end + + def change_emulate_booleans(value) + @connection.emulate_booleans = value + @connection.clear_cache! + end + 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 ecad7c942f..c7531f5418 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -45,8 +45,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns('posts').size - assert_equal 12, @cache.columns_hash('posts').size + assert_equal 11, @cache.columns('posts').size + assert_equal 11, @cache.columns_hash('posts').size assert @cache.tables('posts') assert_equal 'id', @cache.primary_keys('posts') end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb new file mode 100644 index 0000000000..d5c1dc1e5d --- /dev/null +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -0,0 +1,101 @@ +require "cases/helper" + +unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup +module ActiveRecord + module ConnectionAdapters + class TypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_boolean_types + assert_lookup_type :boolean, 'boolean' + assert_lookup_type :boolean, 'BOOLEAN' + end + + def test_string_types + assert_lookup_type :string, 'char' + assert_lookup_type :string, 'varchar' + assert_lookup_type :string, 'VARCHAR' + assert_lookup_type :string, 'varchar(255)' + assert_lookup_type :string, 'character varying' + end + + def test_binary_types + assert_lookup_type :binary, 'binary' + assert_lookup_type :binary, 'BINARY' + assert_lookup_type :binary, 'blob' + assert_lookup_type :binary, 'BLOB' + end + + def test_text_types + assert_lookup_type :text, 'text' + assert_lookup_type :text, 'TEXT' + assert_lookup_type :text, 'clob' + assert_lookup_type :text, 'CLOB' + end + + def test_date_types + assert_lookup_type :date, 'date' + assert_lookup_type :date, 'DATE' + end + + def test_time_types + assert_lookup_type :time, 'time' + assert_lookup_type :time, 'TIME' + end + + def test_datetime_types + assert_lookup_type :datetime, 'datetime' + assert_lookup_type :datetime, 'DATETIME' + assert_lookup_type :datetime, 'timestamp' + assert_lookup_type :datetime, 'TIMESTAMP' + end + + def test_decimal_types + assert_lookup_type :decimal, 'decimal' + assert_lookup_type :decimal, 'decimal(2,8)' + assert_lookup_type :decimal, 'DECIMAL' + assert_lookup_type :decimal, 'numeric' + assert_lookup_type :decimal, 'numeric(2,8)' + assert_lookup_type :decimal, 'NUMERIC' + assert_lookup_type :decimal, 'number' + assert_lookup_type :decimal, 'number(2,8)' + assert_lookup_type :decimal, 'NUMBER' + end + + def test_float_types + assert_lookup_type :float, 'float' + assert_lookup_type :float, 'FLOAT' + assert_lookup_type :float, 'double' + assert_lookup_type :float, 'DOUBLE' + end + + def test_integer_types + assert_lookup_type :integer, 'integer' + assert_lookup_type :integer, 'INTEGER' + assert_lookup_type :integer, 'tinyint' + assert_lookup_type :integer, 'smallint' + assert_lookup_type :integer, 'bigint' + end + + def test_decimal_without_scale + types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal :decimal, cast_type.type + assert_equal 2, cast_type.type_cast_from_user(2.1) + end + end + + private + + def assert_lookup_type(type, lookup) + cast_type = @connection.type_map.lookup(lookup) + assert_equal type, cast_type.type + end + end + end +end +end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 1cf215017b..8d15a76735 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/concurrency/latch' module ActiveRecord module ConnectionAdapters @@ -22,8 +23,7 @@ module ActiveRecord end end - def teardown - super + teardown do @pool.disconnect! end @@ -89,10 +89,9 @@ module ActiveRecord end def test_full_pool_exception + @pool.size.times { @pool.checkout } assert_raises(ConnectionTimeoutError) do - (@pool.size + 1).times do - @pool.checkout - end + @pool.checkout end end @@ -125,7 +124,6 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.dead_connection_timeout = 0 connections = @pool.connections.dup @@ -135,21 +133,25 @@ module ActiveRecord end def test_reap_inactive + ready = ActiveSupport::Concurrency::Latch.new @pool.checkout - @pool.checkout - @pool.checkout - @pool.dead_connection_timeout = 0 - - connections = @pool.connections.dup - connections.each do |conn| - conn.extend(Module.new { def active_threadsafe?; false; end; }) + child = Thread.new do + @pool.checkout + @pool.checkout + ready.release + Thread.stop end + ready.await + + assert_equal 3, active_connections(@pool).size + child.terminate + child.join @pool.reap - assert_equal 0, @pool.connections.length + assert_equal 1, active_connections(@pool).size ensure - connections.each(&:close) + @pool.connections.each(&:close) end def test_remove_connection diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index fdd1914cba..3c2f5d4219 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -82,15 +82,34 @@ module ActiveRecord assert_equal password, spec["password"] end - def test_url_host_db_for_sqlite3 - spec = resolve 'sqlite3://foo:bar@dburl:9000/foo_test' + def test_url_with_authority_for_sqlite3 + spec = resolve 'sqlite3:///foo_test' assert_equal('/foo_test', spec["database"]) end - def test_url_host_memory_db_for_sqlite3 - spec = resolve 'sqlite3://foo:bar@dburl:9000/:memory:' + def test_url_absolute_path_for_sqlite3 + spec = resolve 'sqlite3:/foo_test' + assert_equal('/foo_test', spec["database"]) + end + + def test_url_relative_path_for_sqlite3 + spec = resolve 'sqlite3:foo_test' + assert_equal('foo_test', spec["database"]) + end + + def test_url_memory_db_for_sqlite3 + spec = resolve 'sqlite3::memory:' assert_equal(':memory:', spec["database"]) end + + def test_url_sub_key_for_sqlite3 + spec = resolve :production, 'production' => {"url" => 'sqlite3:foo?encoding=utf8'} + assert_equal({ + "adapter" => "sqlite3", + "database" => "foo", + "encoding" => "utf8" }, spec) + end + end end end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 2a52bf574c..715d92af99 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -1,6 +1,8 @@ require 'cases/helper' require 'models/person' require 'models/topic' +require 'pp' +require 'active_support/core_ext/string/strip' class NonExistentTable < ActiveRecord::Base; end @@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end + + def test_pretty_print_new + topic = Topic.new + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0xXXXXXX + id: nil, + title: nil, + author_name: nil, + author_email_address: "test@test.com", + written_on: nil, + bonus_time: nil, + last_read: nil, + content: nil, + important: nil, + approved: true, + replies_count: 0, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: nil, + updated_at: nil> + PRETTY + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end + + def test_pretty_print_persisted + topic = topics(:first) + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0x\\w+ + id: 1, + title: "The First Topic", + author_name: "David", + author_email_address: "david@loudthinking.com", + written_on: 2003-07-16 14:28:11 UTC, + bonus_time: 2000-01-01 14:28:00 UTC, + last_read: Thu, 15 Apr 2004, + content: "Have a nice day", + important: nil, + approved: false, + replies_count: 1, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: [^,]+, + updated_at: [^,>]+> + PRETTY + assert_match(/\A#{expected}\z/, actual) + end + + def test_pretty_print_uninitialized + topic = Topic.allocate + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = "#<Topic:XXXXXX not initialized>\n" + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ee3d8a81c2..07a182070b 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' + has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply' end class ::SpecialReply < ::Reply @@ -51,6 +52,16 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test "reset counters by counter name" do + # throw the count off by 1 + Topic.increment_counter(:replies_count, @topic.id) + + # check that it gets reset + assert_difference '@topic.reload.replies_count', -1 do + Topic.reset_counters(@topic.id, :replies_count) + end + end + test 'reset multiple counters' do Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1 assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do @@ -154,10 +165,19 @@ class CounterCacheTest < ActiveRecord::TestCase end end - test "the passed symbol needs to be an association name" do + test "the passed symbol needs to be an association name or counter name" do e = assert_raises(ArgumentError) do - Topic.reset_counters(@topic.id, :replies_count) + Topic.reset_counters(@topic.id, :undefined_count) + end + assert_equal "'Topic' has no association called 'undefined_count'", e.message + end + + test "reset counter works with select declared on association" do + special = SpecialTopic.create!(:title => 'Special') + SpecialTopic.increment_counter(:replies_count, special.id) + + assert_difference 'special.reload.replies_count', -1 do + SpecialTopic.reset_counters(special.id, :lightweight_special_replies) end - assert_equal "'Topic' has no association called 'replies_count'", e.message end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 7e3d91e08c..c089e63128 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -18,7 +18,7 @@ class DefaultTest < ActiveRecord::TestCase end end - if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter) + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_default_integers default = Default.new assert_instance_of Fixnum, default.positive_integer @@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) t.column :omit, :integer, :null => false end - assert_equal 0, klass.columns_hash['zero'].default + assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. assert [0, nil].include?(klass.columns_hash['omit'].default) @@ -206,7 +206,12 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" end - def teardown + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end + + teardown do @connection.schema_search_path = @old_search_path Default.reset_column_information end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index df4183c065..69a7f25213 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(:catchphrase => 'Yar!') pirate.catchphrase = 'Ahoy!' - pirate.reset_catchphrase! + assert_deprecated do + pirate.reset_catchphrase! + end + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + + def test_restore_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes assert !pirate.catchphrase_changed? @@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') - pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? - assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + assert_equal ['arr', 'arr'], pirate.catchphrase_change - pirate.catchphrase << '!' + pirate.catchphrase << ' matey!' assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key @@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") pirate_dup = pirate.dup - pirate_dup.reset_catchphrase! + pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? assert !pirate_dup.catchphrase_changed? @@ -445,11 +455,20 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) + + assert_not topic.changed? + topic.content[:b] = "b" - #assert topic.changed? # Known bug, will fail + + assert topic.changed? + topic.save! + + assert_not topic.changed? assert_equal "b", topic.content[:b] + topic.reload + assert_equal "b", topic.content[:b] end end @@ -616,6 +635,32 @@ class DirtyTest < ActiveRecord::TestCase end end + test "defaults with type that implements `type_cast_for_database`" do + type = Class.new(ActiveRecord::Type::Value) do + def type_cast(value) + value.to_i + end + + def type_cast_for_database(value) + value.to_s + end + end + + model_class = Class.new(ActiveRecord::Base) do + self.table_name = 'numeric_data' + attribute :foo, type.new, default: 1 + end + + model = model_class.new + assert_not model.foo_changed? + + model = model_class.new(foo: 1) + assert_not model.foo_changed? + + model = model_class.new(foo: '1') + assert_not model.foo_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 9e268dad74..94447addc1 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -10,7 +10,7 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection end - def teardown + teardown do return if in_memory_db? spec = ActiveRecord::Base.connection_config ActiveRecord::Base.establish_connection(spec) diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 1e6ccecfab..638cffe0e6 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/reply' require 'models/topic' module ActiveRecord @@ -32,6 +33,14 @@ module ActiveRecord assert duped.new_record?, 'topic is new' end + def test_dup_not_destroyed + topic = Topic.first + topic.destroy + + duped = topic.dup + assert_not duped.destroyed? + end + def test_dup_has_no_id topic = Topic.first duped = topic.dup @@ -132,5 +141,17 @@ module ActiveRecord ensure Topic.default_scopes = prev_default_scopes end + + def test_dup_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'parrots_pirates' + end + + record = klass.create! + + assert_nothing_raised do + record.dup + end + end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 1b95708cb3..3b2f0dfe07 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -194,6 +194,7 @@ class EnumTest < ActiveRecord::TestCase :valid, # generates #valid?, which conflicts with an AR method :save, # generates #save!, which conflicts with an AR method :proposed, # same value as an existing enum + :public, :private, :protected, # generates a method that conflict with ruby words ] conflicts.each_with_index do |value, i| @@ -222,4 +223,67 @@ class EnumTest < ActiveRecord::TestCase end end end + + test "validate uniqueness" do + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Book'; end + enum status: [:proposed, :written] + validates_uniqueness_of :status + end + klass.delete_all + klass.create!(status: "proposed") + book = klass.new(status: "written") + assert book.valid? + book.status = "proposed" + assert_not book.valid? + end + + test "validate inclusion of value in array" do + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Book'; end + enum status: [:proposed, :written] + validates_inclusion_of :status, in: ["written"] + end + klass.delete_all + invalid_book = klass.new(status: "proposed") + assert_not invalid_book.valid? + valid_book = klass.new(status: "written") + assert valid_book.valid? + end + + test "enums are distinct per class" do + klass1 = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written] + end + + klass2 = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:drafted, :uploaded] + end + + book1 = klass1.proposed.create! + book1.status = :written + assert_equal ['proposed', 'written'], book1.status_change + + book2 = klass2.drafted.create! + book2.status = :uploaded + assert_equal ['drafted', 'uploaded'], book2.status_change + end + + test "enums are inheritable" do + subklass1 = Class.new(Book) + + subklass2 = Class.new(Book) do + enum status: [:drafted, :uploaded] + end + + book1 = subklass1.proposed.create! + book1.status = :written + assert_equal ['proposed', 'written'], book1.status_change + + book2 = subklass2.drafted.create! + book2.status = :uploaded + assert_equal ['drafted', 'uploaded'], book2.status_change + end end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index b00e2744b9..8de2ddb10d 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -48,7 +48,7 @@ if ActiveRecord::Base.connection.supports_explain? assert queries.empty? end - def teardown + teardown do ActiveRecord::ExplainRegistry.reset end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 6dac5db111..9d25bdd82a 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -26,8 +26,12 @@ if ActiveRecord::Base.connection.supports_explain? sql, binds = queries[0] assert_match "SELECT", sql - assert_match "honda", sql - assert_equal [], binds + if binds.any? + assert_equal 1, binds.length + assert_equal "honda", binds.flatten.last + else + assert_match 'honda', sql + end end def test_exec_explain_with_no_binds diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index b1eded6494..40e51a0cdc 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -33,6 +33,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal(topics(:first).title, Topic.find(1).title) end + def test_find_with_proc_parameter_and_block + exception = assert_raises(RuntimeError) do + Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" } + end + assert_equal "should happen", exception.message + + assert_nothing_raised(RuntimeError) do + Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } + end + end + + def test_find_passing_active_record_object_is_deprecated + assert_deprecated do + Topic.find(Topic.last) + end + end + def test_symbols_table_ref Post.first # warm up x = Symbol.all_symbols.count @@ -56,17 +73,33 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?(id: [1, 9999]) assert_equal false, Topic.exists?(45) - assert_equal false, Topic.exists?(Topic.new) + assert_equal false, Topic.exists?(Topic.new.id) - begin - assert_equal false, Topic.exists?("foo") - rescue ActiveRecord::StatementInvalid - # PostgreSQL complains about string comparison with integer field - rescue Exception - flunk + assert_raise(NoMethodError) { Topic.exists?([1,2]) } + end + + def test_exists_passing_active_record_object_is_deprecated + assert_deprecated do + Topic.exists?(Topic.new) end + end - assert_raise(NoMethodError) { Topic.exists?([1,2]) } + def test_exists_fails_when_parameter_has_invalid_type + if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?(("9"*53).to_i) # number that's bigger than int + end + else + assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int + end + + if current_adapter?(:PostgreSQLAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?("foo") + end + else + assert_equal false, Topic.exists?("foo") + end end def test_exists_does_not_select_columns_without_alias @@ -111,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 37c6af74da..042fdaf0bb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,12 +84,6 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end - def test_create_symbol_fixtures_is_deprecated - assert_deprecated do - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } - end - end - def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) @@ -254,7 +248,7 @@ class FixturesTest < ActiveRecord::TestCase def test_fixtures_are_set_up_with_database_env_variable db_url_tmp = ENV['DATABASE_URL'] - ENV['DATABASE_URL'] = "sqlite3:///:memory:" + ENV['DATABASE_URL'] = "sqlite3::memory:" ActiveRecord::Base.stubs(:configurations).returns({}) test_case = Class.new(ActiveRecord::TestCase) do fixtures :accounts @@ -677,6 +671,12 @@ end class FoxyFixturesTest < ActiveRecord::TestCase fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users" + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + require 'models/uuid_parent' + require 'models/uuid_child' + fixtures :uuid_parents, :uuid_children + end + def test_identifies_strings assert_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("foo")) assert_not_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("FOO")) @@ -689,6 +689,9 @@ class FoxyFixturesTest < ActiveRecord::TestCase def test_identifies_consistently assert_equal 207281424, ActiveRecord::FixtureSet.identify(:ruby) assert_equal 1066363776, ActiveRecord::FixtureSet.identify(:sapphire_2) + + assert_equal 'f92b6bda-0d0d-5fe1-9124-502b18badded', ActiveRecord::FixtureSet.identify(:daddy, :uuid) + assert_equal 'b4b10018-ad47-595d-b42f-d8bdaa6d01bf', ActiveRecord::FixtureSet.identify(:sonny, :uuid) end TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on) @@ -782,6 +785,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert_equal("frederick", parrots(:frederick).name) end + def test_supports_label_string_interpolation + assert_equal("X marks the spot!", pirates(:mark).catchphrase) + end + def test_supports_polymorphic_belongs_to assert_equal(pirates(:redbeard), treasures(:sapphire).looter) assert_equal(parrots(:louis), treasures(:ruby).looter) diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 3758224b0c..6a8aff4b69 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -9,6 +9,7 @@ require 'active_record' require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' +require 'active_support/core_ext/string/strip' require 'support/config' require 'support/connection' @@ -41,6 +42,11 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end +def mysql_56? + current_adapter?(:Mysql2Adapter) && + ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end @@ -106,6 +112,15 @@ def verify_default_timezone_config end end +def enable_uuid_ossp!(connection) + return false unless connection.supports_extensions? + return true if connection.extension_enabled?('uuid-ossp') + + connection.enable_extension 'uuid-ossp' + connection.commit_db_transaction + connection.reconnect! +end + unless ENV['FIXTURE_DEBUG'] module ActiveRecord::TestFixtures::ClassMethods def try_to_load_dependency_with_silence(*args) @@ -163,7 +178,7 @@ class SQLSubscriber def start(name, id, payload) @payloads << payload - @logged << [payload[:sql], payload[:name], payload[:binds]] + @logged << [payload[:sql].squish, payload[:name], payload[:binds]] end def finish(name, id, payload); end @@ -184,3 +199,5 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 367d04a154..b4617cf6f9 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -15,7 +15,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase end teardown do - @klass.connection.drop_table :hot_compatibilities + ActiveRecord::Base.connection.drop_table :hot_compatibilities end test "insert after remove_column" do diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e2ff2aa451..792950d24d 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -95,16 +95,8 @@ class InheritanceTest < ActiveRecord::TestCase end def test_a_bad_type_column - #SQLServer need to turn Identity Insert On before manually inserting into the Identity column - if current_adapter?(:SybaseAdapter) - Company.connection.execute "SET IDENTITY_INSERT companies ON" - end Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')" - #We then need to turn it back Off before continuing. - if current_adapter?(:SybaseAdapter) - Company.connection.execute "SET IDENTITY_INSERT companies OFF" - end assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) } end @@ -339,7 +331,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase ActiveSupport::Dependencies.log_activity = true end - def teardown + teardown do ActiveSupport::Dependencies.log_activity = false self.class.const_remove :FirmOnTheFly rescue nil Firm.const_remove :FirmOnTheFly rescue nil diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index f6774d7ef4..8416c81f45 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -12,7 +12,7 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist' end - def teardown + teardown do Bird.remove_connection end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index debacf815c..285172d33e 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -122,7 +122,7 @@ module ActiveRecord end end - def teardown + teardown do %w[horses new_horses].each do |table| if ActiveRecord::Base.connection.table_exists?(table) ActiveRecord::Base.connection.drop_table(table) @@ -271,16 +271,19 @@ module ActiveRecord ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end - def test_migrate_revert_add_index_with_name - RevertNamedIndexMigration1.new.migrate(:up) - RevertNamedIndexMigration2.new.migrate(:up) - RevertNamedIndexMigration2.new.migrate(:down) - - connection = ActiveRecord::Base.connection - assert connection.index_exists?(:horses, :content), - "index on content should exist" - assert !connection.index_exists?(:horses, :content, name: "horses_index_named"), - "horses_index_named index should not exist" + # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns + unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter) + def test_migrate_revert_add_index_with_name + RevertNamedIndexMigration1.new.migrate(:up) + RevertNamedIndexMigration2.new.migrate(:up) + RevertNamedIndexMigration2.new.migrate(:down) + + connection = ActiveRecord::Base.connection + assert connection.index_exists?(:horses, :content), + "index on content should exist" + assert !connection.index_exists?(:horses, :content, name: "horses_index_named"), + "horses_index_named index should not exist" + end end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index c373dc1511..0c9dff2c25 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end - def test_quoted_locking_column_is_deprecated - assert_deprecated { ActiveRecord::Base.quoted_locking_column } + def test_yaml_dumping_with_lock_column + t1 = LockWithoutDefault.new + t2 = YAML.load(YAML.dump(t1)) + + assert_equal t1.attributes, t2.attributes end end @@ -339,8 +342,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase def add_counter_column_to(model, col='test_count') model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0 model.reset_column_information - # OpenBase does not set a value to existing rows when adding a not null default column - model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter) end def remove_counter_column_from(model, col = :test_count) @@ -367,7 +368,7 @@ end # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute # blocks, so separate script called by Kernel#system is needed. # (See exec vs. async_exec in the PostgreSQL adapter.) -unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? +unless in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false fixtures :people, :readers diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 97c0350911..a578e81844 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -125,5 +125,12 @@ class LogSubscriberTest < ActiveRecord::TestCase wait assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) end + + def test_nil_binary_data_is_logged + binary = Binary.create(data: "") + binary.update_attributes(data: nil) + wait + assert_match(/<NULL binary data>/, @logger.logged(:debug).join) + end end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 294f2eb9fe..c66eaf1ee1 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -11,10 +11,10 @@ module ActiveRecord @table_name = :testings end - def teardown - super + teardown do connection.drop_table :testings rescue nil ActiveRecord::Base.primary_key_prefix_type = nil + ActiveRecord::Base.clear_cache! end def test_create_table_without_id @@ -68,9 +68,9 @@ module ActiveRecord 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 true, two.type_cast_from_database(two.default) + assert_equal false, three.type_cast_from_database(three.default) + assert_equal '1', four.default assert_equal "hello", five.default unless mysql end @@ -205,9 +205,9 @@ module ActiveRecord connection.create_table table_name end - # Sybase, and SQLite3 will not allow you to add a NOT NULL + # SQLite3 will not allow you to add a NOT NULL # column to a table without a default value. - unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) + unless current_adapter?(:SQLite3Adapter) def test_add_column_not_null_without_default connection.create_table :testings do |t| t.column :foo, :string @@ -226,18 +226,28 @@ module ActiveRecord 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 + connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" + end + end + + def test_add_column_with_timestamp_type + connection.create_table :testings do |t| + t.column :foo, :timestamp + end + + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'testings' + + assert_equal :datetime, klass.columns_hash['foo'].type + + if current_adapter?(:PostgreSQLAdapter) + assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type + else + assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type end end @@ -265,7 +275,7 @@ module ActiveRecord 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 99, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # Oracle needs primary key value from sequence if current_adapter?(:OracleAdapter) @@ -277,20 +287,20 @@ module ActiveRecord # 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 100, person_klass.column_defaults["wealth"] 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 100, person_klass.column_defaults["money"] 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 1000, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column, make it nullable and clear default diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index c1d7cd5874..3e9d957ed3 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -8,7 +8,7 @@ module ActiveRecord @connection = Minitest::Mock.new end - def teardown + teardown do assert @connection.verify end @@ -72,6 +72,20 @@ module ActiveRecord end end + def test_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.references :taggable, polymorphic: true, type: :string + end + end + + def test_remove_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.remove_references :taggable, polymorphic: true, type: :string + end + end + def test_timestamps_creates_updated_at_and_created_at with_change_table do |t| @connection.expect :add_timestamps, nil, [:delete_me] diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index ccf19fb4d0..763aa88f72 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -35,6 +35,14 @@ module ActiveRecord assert_no_column TestModel, :last_name end + def test_add_column_without_limit + # TODO: limit: nil should work with all adapters. + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + add_column :test_models, :description, :string, limit: nil + TestModel.reset_column_information + assert_nil TestModel.columns_hash["description"].limit + end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_unabstracted_database_dependent_types add_column :test_models, :intelligence_quotient, :tinyint @@ -43,46 +51,46 @@ module ActiveRecord end 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) values (people_seq.nextval, 12345678901234567890.0123456789)" - 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) values ('12345678901234567890.0123456789')" - elsif current_adapter?(:PostgreSQLAdapter) - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - else - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - end + unless current_adapter?(:SQLite3Adapter) + # 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) values (people_seq.nextval, 12345678901234567890.0123456789)" + elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings + connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" + elsif current_adapter?(:PostgreSQLAdapter) + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + else + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + end - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.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 + # 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 + # Reset to old state + TestModel.delete_all - # Now use the Rails insertion - TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") + # Now use the Rails insertion + TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.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) + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! assert_equal correct_value, row.wealth end end @@ -113,54 +121,54 @@ module ActiveRecord end 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.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) + unless current_adapter?(:SQLite3Adapter) + 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.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. + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth - end - assert_equal true, bob.male? + 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 + 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 + if current_adapter?(:OracleAdapter) + # Oracle doesn't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end - assert_instance_of TrueClass, bob.male? - assert_kind_of BigDecimal, bob.wealth + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end end if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 87e29e41ba..77a752f050 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -18,8 +18,7 @@ module ActiveRecord end end - def teardown - super + teardown do connection.drop_table :testings rescue nil ActiveRecord::Base.primary_key_prefix_type = nil end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 2d7a7ec73a..4e6d7963aa 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -53,13 +53,13 @@ module ActiveRecord 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 + assert_equal '70000', default_before rename_column "test_models", "salary", "annual_salary" assert TestModel.column_names.include?("annual_salary") default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default - assert_equal 70000, default_after + assert_equal '70000', default_after end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) @@ -193,14 +193,21 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - c.name == 'approved' && c.type == :boolean && c.default == true + default = c.type_cast_from_database(c.default) + c.name == 'approved' && c.type == :boolean && default == true } change_column :test_models, :approved, :boolean, :default => false new_columns = connection.columns(TestModel.table_name) - assert_not 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_not new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == true + } + assert new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == false + } change_column :test_models, :approved, :boolean, :default => true end @@ -274,6 +281,16 @@ module ActiveRecord ensure connection.drop_table(:my_table) rescue nil end + + def test_column_with_index + connection.create_table "my_table", force: true do |t| + t.string :item_number, index: true + end + + assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number) + ensure + connection.drop_table(:my_table) rescue nil + 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 a925cf4c05..e955beae1a 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -157,6 +157,23 @@ module ActiveRecord assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove end + def test_invert_change_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column, [:table, :column, :type, {}] + end + end + + def test_invert_change_column_default + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_default, [:table, :column, 'default_value'] + end + end + + def test_invert_change_column_null + add = @recorder.inverse_of :change_column_null, [:table, :column, true] + assert_equal [:change_column_null, [:table, :column, false]], add + end + def test_invert_remove_column add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}] assert_equal [:add_column, [:table, :column, :type, {}], nil], add @@ -253,6 +270,31 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + end end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index efaec0f823..bea9d6b2c9 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -10,8 +10,7 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end - def teardown - super + teardown do %w(artists_musics musics_videos catalog).each do |table_name| connection.drop_table table_name if connection.tables.include?(table_name) end @@ -120,6 +119,30 @@ module ActiveRecord assert !connection.tables.include?('artists_musics') end + + def test_create_and_drop_join_table_with_common_prefix + with_table_cleanup do + connection.create_join_table 'audio_artists', 'audio_musics' + assert_includes connection.tables, 'audio_artists_musics' + + connection.drop_join_table 'audio_artists', 'audio_musics' + assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + end + end + + private + + def with_table_cleanup + tables_before = connection.tables + + yield + ensure + tables_after = connection.tables - tables_before + + tables_after.each do |table| + connection.execute "DROP TABLE #{table}" + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..c985092b4c --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,242 @@ +require 'cases/helper' +require 'support/ddl_helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + include SchemaDumpingHelper + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets" do |t| + t.string :name + end + + @connection.create_table "astronauts" do |t| + t.string :name + t.references :rocket + end + end + + teardown do + if defined?(@connection) + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + end + end +end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 8d1daa0a04..ac932378fd 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -21,34 +21,30 @@ module ActiveRecord end end - def teardown - super + teardown do connection.drop_table :testings rescue nil ActiveRecord::Base.primary_key_prefix_type = nil end - unless current_adapter?(:OpenBaseAdapter) - def test_rename_index - # 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') + def test_rename_index + # 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 - assert_not connection.index_name_exists?(table_name, 'old_idx', false) - assert connection.index_name_exists?(table_name, 'new_idx', true) - end + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + assert_not 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 + def test_double_add_index + connection.add_index(table_name, [:foo], :name => 'some_idx') + assert_raises(ArgumentError) { connection.add_index(table_name, [:foo], :name => 'some_idx') - assert_raises(ArgumentError) { - connection.add_index(table_name, [:foo], :name => 'some_idx') - } - end + } + end - def test_remove_nonexistent_index - # 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_remove_nonexistent_index + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } end def test_add_index_works_with_long_index_names @@ -99,6 +95,12 @@ module ActiveRecord assert connection.index_exists?(:testings, [:foo, :bar]) end + def test_index_exists_with_custom_name_checks_columns + connection.add_index :testings, [:foo, :bar], name: "my_index" + assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index") + assert_not connection.index_exists?(:testings, [:foo], name: "my_index") + end + def test_valid_index_options assert_raise ArgumentError do connection.add_index :testings, :foo, unqiue: true @@ -127,50 +129,37 @@ module ActiveRecord 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", :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.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", "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 => 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"], :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 => 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 + connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) + connection.remove_index("testings", ["last_name", "first_name"]) - # 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 + connection.add_index("testings", ["key"], :name => "key_idx", :unique => true) + connection.remove_index("testings", :name => "key_idx", :unique => true) - # 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 + connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin") + connection.remove_index("testings", :name => "named_admin") # Selected adapters support index sort order if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 97efb94b66..319d3e1af3 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase - # mysql can't roll back ddl changes + # MySQL can't roll back ddl changes self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do @@ -19,8 +19,7 @@ module ActiveRecord ActiveRecord::SchemaMigration.delete_all end - def teardown - super + teardown do ActiveRecord::SchemaMigration.drop_table end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..7afac83bd2 --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,53 @@ +require 'cases/helper' +require "minitest/mock" + +module ActiveRecord + class Migration + class PendingMigrationsTest < ActiveRecord::TestCase + def setup + super + @connection = Minitest::Mock.new + @app = Minitest::Mock.new + conn = @connection + @pending = Class.new(CheckPending) { + define_method(:connection) { conn } + }.new(@app) + @pending.instance_variable_set :@last_check, -1 # Force checking + end + + def teardown + assert @connection.verify + assert @app.verify + super + end + + def test_errors_if_pending + @connection.expect :supports_migrations?, true + + ActiveRecord::Migrator.stub :needs_migration?, true do + assert_raise ActiveRecord::PendingMigrationError do + @pending.call(nil) + end + end + end + + def test_checks_if_supported + @connection.expect :supports_migrations?, true + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, false do + @pending.call(:foo) + end + end + + def test_doesnt_check_if_unsupported + @connection.expect :supports_migrations?, false + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, true do + @pending.call(:foo) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 19eb7d3c9e..4485701a4e 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -11,8 +11,7 @@ module ActiveRecord @table_name = :testings end - def teardown - super + teardown do connection.drop_table :testings rescue nil end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index e9545f2cce..b8b4fa1135 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_reference_id_with_specified_type + add_reference table_name, :user, type: :string + assert column_exists?(table_name, :user_id, :string) + end + def test_deletes_reference_id_column remove_reference table_name, :supplier assert_not column_exists?(table_name, :supplier_id, :integer) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 2a7fafc559..ba39fb1dec 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -42,13 +42,8 @@ module ActiveRecord def test_rename_table rename_table :test_models, :octopi - # Using explicit id in insert for compatibility across all databases - connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - connection.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") end @@ -57,10 +52,7 @@ module ActiveRecord rename_table :test_models, :octopi - # Using explicit id in insert for compatibility across all databases - connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - connection.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") index = connection.indexes(:octopi).first @@ -82,7 +74,17 @@ module ActiveRecord pk, seq = connection.pk_and_sequence_for('octopi') - assert_equal "octopi_#{pk}_seq", seq + assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq + end + + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences + enable_uuid_ossp!(connection) + connection.create_table :cats, id: :uuid + assert_nothing_raised { rename_table :cats, :felines } + assert connection.table_exists? :felines + ensure + connection.drop_table :cats if connection.table_exists? :cats + connection.drop_table :felines if connection.table_exists? :felines end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 1bda472d23..ef3f073472 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things" require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" -class BigNumber < ActiveRecord::Base; end +class BigNumber < ActiveRecord::Base + unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + attribute :value_of_e, Type::Integer.new + end + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new +end class Reminder < ActiveRecord::Base; end @@ -33,7 +39,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.connection.schema_cache.clear! end - def teardown + teardown do ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" @@ -57,7 +63,7 @@ class MigrationTest < ActiveRecord::TestCase 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.connection.add_column("people", "first_name", :string) Person.reset_column_information end @@ -327,47 +333,6 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end - def test_proper_table_name_on_migrator - reminder_class = new_isolated_reminder_class - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - end - assert_deprecated do - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.reset_table_name - assert_deprecated do - assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class) - end - - # 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_class.table_name_prefix = 'prefix_' - reminder_class.table_name_suffix = '_suffix' - reminder_class.reset_table_name - assert_deprecated do - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.table_name_prefix = '' - reminder_class.table_name_suffix = '' - reminder_class.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_class.reset_table_name - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) - end - end - def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new @@ -585,7 +550,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? Person.reset_sequence_name end - def teardown + teardown do Person.connection.drop_table(:delete_me) rescue nil end @@ -602,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default + assert_equal '0', column(:age).default end def test_removing_columns @@ -930,4 +895,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.logger = old end + + private + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 3f9854200d..9568aa2217 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -26,8 +26,7 @@ module ActiveRecord ActiveRecord::SchemaMigration.delete_all rescue nil end - def teardown - super + teardown do ActiveRecord::SchemaMigration.delete_all rescue nil ActiveRecord::Migration.verbose = true end @@ -93,7 +92,7 @@ module ActiveRecord def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid/") + ActiveRecord::Migrator.migrations("valid") end migration_proxy = list.find { |item| diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 9124105e6d..e87773df94 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -18,7 +18,7 @@ class ModulesTest < ActiveRecord::TestCase ActiveRecord::Base.store_full_sti_class = false end - def teardown + teardown do # reinstate the constants that we undefined in the setup @undefined_consts.each do |constant, value| Object.send :const_set, constant, value unless value.nil? @@ -112,6 +112,34 @@ class ModulesTest < ActiveRecord::TestCase classes.each(&:reset_table_name) end + def test_module_table_name_suffix + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix' + assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed' + end + + def test_module_table_name_suffix_with_global_suffix + classes = [ MyApplication::Business::Company, + MyApplication::Business::Firm, + MyApplication::Business::Client, + MyApplication::Business::Client::Contact, + MyApplication::Business::Developer, + MyApplication::Business::Project, + MyApplication::Business::Suffixed::Company, + MyApplication::Business::Suffixed::Nested::Company, + MyApplication::Billing::Account ] + + ActiveRecord::Base.table_name_suffix = '_global' + classes.each(&:reset_table_name) + assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix' + assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed' + ensure + ActiveRecord::Base.table_name_suffix = '' + classes.each(&:reset_table_name) + end + def test_compute_type_can_infer_class_name_of_sibling_inside_module old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index c70a8f296f..14d4ef457d 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -240,8 +240,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase Topic.skip_time_zone_conversion_for_attributes = [] end - # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + unless current_adapter?(:OracleAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do attributes = { diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 2f89699df7..cf96c3fccf 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -11,24 +11,8 @@ require "models/owner" require "models/pet" require 'active_support/hash_with_indifferent_access' -module AssertRaiseWithMessage - def assert_raise_with_message(expected_exception, expected_message) - begin - error_raised = false - yield - rescue expected_exception => error - error_raised = true - actual_message = error.message - end - assert error_raised - assert_equal expected_message, actual_message - end -end - class TestNestedAttributesInGeneral < ActiveRecord::TestCase - include AssertRaiseWithMessage - - def teardown + teardown do Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } end @@ -71,9 +55,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_should_raise_an_ArgumentError_for_non_existing_associations - assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do + exception = assert_raise ArgumentError do Pirate.accepts_nested_attributes_for :honesty end + assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message end def test_should_disable_allow_destroy_by_default @@ -213,17 +198,16 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase - include AssertRaiseWithMessage - def setup @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to - assert_raise_with_message ArgumentError, "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?" do + exception = assert_raise ArgumentError do Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"}) end + assert_equal "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?", exception.message end def test_should_define_an_attribute_writer_method_for_the_association @@ -275,9 +259,10 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + exception = assert_raise ActiveRecord::RecordNotFound do @pirate.ship_attributes = { :id => 1234567890 } end + assert_equal "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @@ -403,8 +388,6 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase - include AssertRaiseWithMessage - def setup @ship = Ship.new(:name => 'Nights Dirty Lightning') @pirate = @ship.build_pirate(:catchphrase => 'Aye') @@ -460,9 +443,10 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do + exception = assert_raise ActiveRecord::RecordNotFound do @ship.pirate_attributes = { :id => 1234567890 } end + assert_equal "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}", exception.message end def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @@ -579,8 +563,6 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end module NestedAttributesOnACollectionAssociationTests - include AssertRaiseWithMessage - def test_should_define_an_attribute_writer_method_for_the_association assert_respond_to @pirate, association_setter end @@ -670,9 +652,10 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + exception = assert_raise ActiveRecord::RecordNotFound do @pirate.attributes = { association_getter => [{ :id => 1234567890 }] } end + assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @@ -727,9 +710,10 @@ module NestedAttributesOnACollectionAssociationTests assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } - assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do + exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end + assert_equal 'Hash or Array expected, got String ("foo")', exception.message end def test_should_work_with_update_as_well diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b9f0624f76..2170fe6118 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/aircraft' require 'models/post' require 'models/comment' require 'models/author' @@ -19,7 +20,7 @@ require 'models/toy' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -233,6 +234,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_nothing_raised { Minimalistic.create!(:id => 2) } end + def test_save_with_duping_of_destroyed_object + developer = Developer.first + developer.destroy + new_developer = developer.dup + new_developer.save + assert new_developer.persisted? + assert_not new_developer.destroyed? + end + def test_create_many topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) assert_equal 2, topics.size @@ -240,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.allocate.init_with( - 'attributes' => { - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' - } + topic = Topic.instantiate( + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' ) assert_nothing_raised { topic.save } end @@ -290,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topic_reloaded = Topic.allocate - topic_reloaded.init_with( - 'attributes' => topic.attributes.merge('does_not_exist' => 'test') - ) + topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test')) topic_reloaded.title = 'A New Topic' assert_nothing_raised { topic_reloaded.save } end @@ -323,6 +328,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Reply", topic.type end + def test_update_sti_subclass_type + assert_instance_of Topic, topics(:first) + + reply = topics(:first).becomes!(Reply) + assert_instance_of Reply, reply + reply.save! + assert_instance_of Reply, Reply.find(reply.id) + end + def test_update_after_create klass = Class.new(Topic) do def self.name; 'Topic'; end @@ -452,7 +466,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_attribute_for_updated_at_on developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_attribute(:updated_at, prev_month) assert_equal prev_month, developer.updated_at @@ -495,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") topic.reload - topic.update_column(:content, "You too") + topic.update_column(:content, "--- You too\n...\n") assert_equal [], topic.changed topic.reload - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") assert_equal [], topic.changed end @@ -523,7 +537,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column_should_not_modify_updated_at developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_column(:updated_at, prev_month) assert_equal prev_month, developer.updated_at @@ -586,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update({ "content" => "Have a nice day", :author_name => "Jose" }) + topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" }) topic.reload - topic.update_columns({ content: "You too", "author_name" => "Sebastian" }) + topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" }) assert_equal [], topic.changed topic.reload - topic.update_columns({ content: "Have a nice day", author_name: "Jose" }) + topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" }) assert_equal [], topic.changed end @@ -620,7 +634,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns_should_not_modify_updated_at developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_columns(updated_at: prev_month) assert_equal prev_month, developer.updated_at @@ -820,4 +834,47 @@ class PersistenceTest < ActiveRecord::TestCase end end + def test_persist_inherited_class_with_different_table_name + minimalistic_aircrafts = Class.new(Minimalistic) do + self.table_name = "aircraft" + end + + assert_difference "Aircraft.count", 1 do + aircraft = minimalistic_aircrafts.create(name: "Wright Flyer") + aircraft.name = "Wright Glider" + aircraft.save + end + + assert_equal "Wright Glider", Aircraft.last.name + end + + def test_instantiate_creates_a_new_instance + post = Post.instantiate("title" => "appropriate documentation", "type" => "SpecialPost") + assert_equal "appropriate documentation", post.title + assert_instance_of SpecialPost, post + + # body was not initialized + assert_raises ActiveModel::MissingAttributeError do + post.body + end + end + + def test_reload_removes_custom_selects + post = Post.select('posts.*, 1 as wibble').last! + + assert_equal 1, post[:wibble] + assert_nil post.reload[:wibble] + end + + def test_find_via_reload + post = Post.new + + assert post.new_record? + + post.id = 1 + post.reload + + assert_equal "Welcome to the weblog", post.title + assert_not post.new_record? + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 626c6aeaf8..8eea10143f 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -10,7 +10,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.remove_connection end - def teardown + teardown do ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) @per_test_teardown.each {|td| td.call } @@ -48,4 +48,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase def add_record(name) ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name } end -end unless current_adapter?(:FrontBase) || in_memory_db? +end unless in_memory_db? diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 51ddd406ed..f19a6ea5e3 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -5,6 +5,7 @@ require 'models/subscriber' require 'models/movie' require 'models/keyboard' require 'models/mixed_case_monkey' +require 'models/dashboard' class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -92,6 +93,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_prefix + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name Topic.reset_primary_key assert_equal "topicid", Topic.primary_key @@ -103,6 +105,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase ActiveRecord::Base.primary_key_prefix_type = nil Topic.reset_primary_key assert_equal "id", Topic.primary_key + ensure + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end def test_delete_should_quote_pkey @@ -131,14 +135,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers') + assert_equal 'id', klass.primary_key end end def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_nil ActiveRecord::Base.connection.primary_key('developers_projects') + assert_nil klass.primary_key end end @@ -149,42 +161,19 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("foo"), 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 - def test_auto_detect_primary_key_from_schema MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key end + + def test_primary_key_update_with_custom_key_name + dashboard = Dashboard.create!(dashboard_id: '1') + dashboard.id = '2' + dashboard.save! + + dashboard = Dashboard.first + assert_equal '2', dashboard.id + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase @@ -219,3 +208,29 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end end + +if current_adapter?(:PostgreSQLAdapter) + class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Widget < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:widgets, id: :bigserial) { |t| } + end + + teardown do + @connection.drop_table :widgets + end + + def test_bigserial_primary_key + assert_equal "id", Widget.primary_key + assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + + widget = Widget.create! + assert_not_nil widget.id + end + end +end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index da8ae672fe..9d89d6a1e8 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -118,6 +118,14 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty' end + def test_cache_passing_a_relation + post = Post.first + Post.cache do + query = post.categories.select(:post_id) + assert Post.connection.select_all(query).is_a?(ActiveRecord::Result) + end + end + def test_find_queries assert_queries(2) { Task.find(1); Task.find(1) } end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index e2439b9a24..1d6ae2f67f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -3,14 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class QuotingTest < ActiveRecord::TestCase - class FakeColumn < ActiveRecord::ConnectionAdapters::Column - attr_accessor :type - - def initialize type - @type = type - end - end - def setup @quoter = Class.new { include Quoting }.new end @@ -91,69 +83,56 @@ module ActiveRecord def test_quote_with_quoted_id assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') end def test_quote_nil assert_equal 'NULL', @quoter.quote(nil, nil) - assert_equal 'NULL', @quoter.quote(nil, 'foo') end def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer)) end def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) end def test_crazy_object crazy = Class.new.new expected = "'#{YAML.dump(crazy)}'" assert_equal expected, @quoter.quote(crazy, nil) - assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string crazy = Class.new { def initialize; @lol = 'lo\l' end }.new assert_match "lo\\\\l", @quoter.quote(crazy, nil) - assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column @@ -165,36 +144,13 @@ module ActiveRecord assert_equal "'lo\\\\l'", @quoter.quote(string, nil) end - def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer)) - assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer)) - end - - def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float)) - assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float)) - end - - def test_quote_as_mb_chars_binary_column - string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary)) - end - - def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) - end - def test_string_with_crazy_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end def test_quote_duration assert_equal "1800", @quoter.quote(30.minutes) end - - def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) - end end end end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b62a41c08e..f52fd22489 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -10,8 +10,7 @@ module ActiveRecord @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec end - def teardown - super + teardown do @pool.connections.each(&:close) end @@ -64,17 +63,22 @@ module ActiveRecord spec.config[:reaping_frequency] = 0.0001 pool = ConnectionPool.new spec - pool.dead_connection_timeout = 0 - conn = pool.checkout - count = pool.connections.length + conn = nil + child = Thread.new do + conn = pool.checkout + Thread.stop + end + Thread.pass while conn.nil? + + assert conn.in_use? - conn.extend(Module.new { def active_threadsafe?; false; end; }) + child.terminate - while count == pool.connections.length + while conn.in_use? Thread.pass end - assert_equal(count - 1, pool.connections.length) + assert !conn.in_use? end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index d7ad5ed29f..84abaf0291 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -63,7 +63,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_column_string_type_and_limit assert_equal :string, @first.column_for_attribute("title").type - assert_equal 255, @first.column_for_attribute("title").limit + assert_equal 250, @first.column_for_attribute("title").limit end def test_column_null_not_null @@ -80,24 +80,38 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end + def test_non_existent_columns_return_nil + assert_deprecated do + assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") + end + end + def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end end + def test_irregular_reflection_class_name + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'plural_irregular', 'plurales_irregulares' + end + reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + assert_equal 'PluralIrregular', reflection.class_name + end + def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, nil, { }, Customer + :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -121,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -133,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -192,7 +206,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_should_not_raise_error_when_compared_to_other_object - assert_nothing_raised { Firm.reflections[:clients] == Object.new } + assert_not_equal Object.new, Firm._reflections['clients'] + end + + def test_has_and_belongs_to_many_reflection + assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro + assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name end def test_has_many_through_reflection @@ -265,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) + reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } through = Class.new(ActiveRecord::Reflection::ThroughReflection) { define_method(:source_reflection) { reflection } - }.new(:fuu, :edge, nil, {}, Author) + }.new(reflection) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -280,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) + reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -298,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -345,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -358,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'catalog_categories_products', reflection.join_table end @@ -371,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_content_pages', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) reflection.stubs(:klass).returns(page) assert_equal 'catalog_categories_content_pages', reflection.join_table end @@ -384,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) reflection.stubs(:klass).returns(category) assert_equal 'product_categories', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) reflection.stubs(:klass).returns(product) assert_equal 'product_categories', reflection.join_table end + def test_includes_accepts_symbols + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs + end + end + + def test_includes_accepts_strings + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs + end + end + + def test_reflect_on_association_accepts_symbols + assert_nothing_raised do + assert_equal Hotel.reflect_on_association(:departments).name, :departments + end + end + + def test_reflect_on_association_accepts_strings + assert_nothing_raised do + assert_equal Hotel.reflect_on_association("departments").name, :departments + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 9b2bfed039..29c9d0e2af 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -32,7 +32,7 @@ module ActiveRecord :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, - :to_ary, :to_set, :to_xml, :to_yaml + :to_ary, :to_set, :to_xml, :to_yaml, :join ] ARRAY_DELEGATES.each do |method| diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 23500bf5d8..2b5c2fd5a4 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -17,10 +17,9 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_relation_to_sql - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql - end - assert_no_match(/\?/, sql) + post = Post.first + sql = post.comments.to_sql + assert_match(/.?post_id.? = #{post.id}\Z/i, sql) end def test_relation_merging_with_arel_equalities_keeps_last_equality @@ -81,31 +80,20 @@ class RelationMergingTest < ActiveRecord::TestCase left = Post.where(title: "omg").where(comments_count: 1) right = Post.where(title: "wtf").where(title: "bbq") - expected = [left.where_values[1]] + right.where_values + expected = [left.bind_values[1]] + right.bind_values merged = left.merge(right) - assert_equal expected, merged.where_values + assert_equal expected, merged.bind_values assert !merged.to_sql.include?("omg") assert merged.to_sql.include?("wtf") assert merged.to_sql.include?("bbq") end - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) - - merged = left.merge(right) - assert_equal [], merged.bind_values - end - def test_merging_keeps_lhs_bind_parameters column = Post.columns_hash['id'] binds = [[column, 20]] - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds + right = Post.where(id: 20) left = Post.where(id: 10) merged = left.merge(right) @@ -113,21 +101,18 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] + post = Post.first + right = Post.where(id: 1) + left = Post.where(title: post.title) merged = left.merge(right) assert_equal post, merged.first end + + def test_merging_compares_symbols_and_strings_as_equal + post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.") + assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 4fafa668fb..1da5c36e1c 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -24,13 +24,18 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") end end + test "#_select!" do + assert relation.public_send("_select!", :foo).equal?(relation) + assert_equal [:foo], relation.public_send("select_values") + end + test '#order!' do assert relation.order!('name ASC').equal?(relation) assert_equal ['name ASC'], relation.order_values @@ -107,10 +112,18 @@ module ActiveRecord end test 'reverse_order!' do - assert relation.reverse_order!.equal?(relation) - assert relation.reverse_order_value + relation = Post.order('title ASC, comments_count DESC') + + relation.reverse_order! + + assert_equal 'title DESC', relation.order_values.first + assert_equal 'comments_count ASC', relation.order_values.last + + relation.reverse_order! - assert !relation.reverse_order_value + + assert_equal 'title ASC', relation.order_values.first + assert_equal 'comments_count DESC', relation.order_values.last end test 'create_with!' do diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 14a8d97d36..4057835688 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -5,10 +5,10 @@ module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers PredicateBuilder.register_handler(Regexp, proc do |column, value| - Arel::Nodes::InfixOperation.new('~', column, value.source) + Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) - assert_match %r{["`]topics["`].["`]title["`] ~ 'rails'}i, Topic.where(title: /rails/).to_sql + assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql end end end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index fd2420cb88..b9e69bdb08 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -12,13 +12,19 @@ module ActiveRecord end def test_not_eq - expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') relation = Post.where.not(title: 'hello') - assert_equal([expected], relation.where_values) + + assert_equal 1, relation.where_values.length + + value = relation.where_values.first + bind = relation.bind_values.first + + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'hello', bind.last end def test_not_null - expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil) + expected = Post.arel_table[@name].not_eq(nil) relation = Post.where.not(title: nil) assert_equal([expected], relation.where_values) end @@ -30,13 +36,13 @@ module ActiveRecord end def test_not_in - expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye]) + expected = Post.arel_table[@name].not_in(%w[hello goodbye]) relation = Post.where.not(title: %w[hello goodbye]) assert_equal([expected], relation.where_values) end def test_association_not_eq - expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello') + expected = Comment.arel_table[@name].not_eq('hello') relation = Post.joins(:comments).where.not(comments: {title: 'hello'}) assert_equal(expected.to_sql, relation.where_values.first.to_sql) end @@ -44,21 +50,29 @@ module ActiveRecord def test_not_eq_with_preceding_where relation = Post.where(title: 'hello').where.not(title: 'world') - expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello') - assert_equal(expected, relation.where_values.first) + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'hello', bind.last - expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world') - assert_equal(expected, relation.where_values.last) + value = relation.where_values.last + bind = relation.bind_values.last + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'world', bind.last end def test_not_eq_with_succeeding_where relation = Post.where.not(title: 'hello').where(title: 'world') - expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') - assert_equal(expected, relation.where_values.first) + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'hello', bind.last - expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world') - assert_equal(expected, relation.where_values.last) + value = relation.where_values.last + bind = relation.bind_values.last + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'world', bind.last end def test_not_eq_with_string_parameter @@ -76,41 +90,64 @@ module ActiveRecord def test_chaining_multiple relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails') - expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2]) + expected = Post.arel_table['author_id'].not_in([1, 2]) assert_equal(expected, relation.where_values[0]) - expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails') - assert_equal(expected, relation.where_values[1]) + value = relation.where_values[1] + bind = relation.bind_values.first + + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'ruby on rails', bind.last end - + def test_rewhere_with_one_condition relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') - expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'alone') assert_equal 1, relation.where_values.size - assert_equal expected, relation.where_values.first + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'alone', bind.last end def test_rewhere_with_multiple_overwriting_conditions relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again') - title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') - body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'again') - assert_equal 2, relation.where_values.size - assert_equal title_expected, relation.where_values.first - assert_equal body_expected, relation.where_values.second + + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality + assert_equal 'alone', bind.last + + value = relation.where_values[1] + bind = relation.bind_values[1] + assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality + assert_equal 'again', bind.last + end + + def assert_bound_ast value, table, type + assert_equal table, value.left + assert_kind_of type, value + assert_kind_of Arel::Nodes::BindParam, value.right end def test_rewhere_with_one_overwriting_condition_and_one_unrelated relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') - title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') - body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'world') - assert_equal 2, relation.where_values.size - assert_equal body_expected, relation.where_values.first - assert_equal title_expected, relation.where_values.second + + value = relation.where_values.first + bind = relation.bind_values.first + + assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality + assert_equal 'world', bind.last + + value = relation.where_values.second + bind = relation.bind_values.second + + assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality + assert_equal 'alone', bind.last end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 937f226b1d..a6a36a6fd9 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -6,10 +6,11 @@ require 'models/post' require 'models/comment' require 'models/edge' require 'models/topic' +require 'models/binary' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors + fixtures :posts, :edges, :authors, :binaries def test_where_copies_bind_params author = authors(:david) @@ -179,5 +180,35 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end + + def test_where_with_integer_for_binary_column + count = Binary.where(:data => 0).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 15611656fd..3280945d09 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -16,6 +16,10 @@ module ActiveRecord def self.connection Post.connection end + + def self.table_name + 'fake_table' + end end def test_construction @@ -231,5 +235,33 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value + def type + :string + end + + def type_cast_from_database(value) + raise value unless value == "type cast for database" + "type cast from database" + end + + def type_cast_for_database(value) + raise value unless value == "value from user" + "type cast for database" + end + end + + class UpdateAllTestModel < ActiveRecord::Base + self.table_name = 'posts' + + attribute :body, EnsureRoundTripTypeCasting.new + end + + def test_update_all_goes_through_normal_type_casting + UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI + + assert_equal "type cast from database", UpdateAllTestModel.first.body + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index e390d37871..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -14,6 +14,7 @@ require 'models/car' require 'models/engine' require 'models/tyre' require 'models/minivan' +require 'models/aircraft' class RelationTest < ActiveRecord::TestCase @@ -151,11 +152,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a end - def test_finding_with_subquery_without_select - relation = Topic.where(:approved => true) - assert_equal relation.to_a, Topic.from(relation).to_a + def test_finding_with_subquery_without_select_does_not_change_the_select + relation = Topic.where(approved: true) + assert_raises(ActiveRecord::StatementInvalid) do + Topic.from(relation).to_a + end end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -168,7 +172,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:first).title, topics.first.title end - def test_finding_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) assert_equal 5, topics.to_a.size @@ -191,8 +194,33 @@ class RelationTest < ActiveRecord::TestCase assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql end + def test_finding_with_desc_order_with_string + topics = Topic.order(id: "desc") + assert_equal 5, topics.to_a.size + assert_equal [topics(:fifth), topics(:fourth), topics(:third), topics(:second), topics(:first)], topics.to_a + end + + def test_finding_with_asc_order_with_string + topics = Topic.order(id: 'asc') + assert_equal 5, topics.to_a.size + assert_equal [topics(:first), topics(:second), topics(:third), topics(:fourth), topics(:fifth)], topics.to_a + end + + def test_support_upper_and_lower_case_directions + assert_includes Topic.order(id: "ASC").to_sql, "ASC" + assert_includes Topic.order(id: "asc").to_sql, "ASC" + assert_includes Topic.order(id: :ASC).to_sql, "ASC" + assert_includes Topic.order(id: :asc).to_sql, "ASC" + + assert_includes Topic.order(id: "DESC").to_sql, "DESC" + assert_includes Topic.order(id: "desc").to_sql, "DESC" + assert_includes Topic.order(id: :DESC).to_sql, "DESC" + assert_includes Topic.order(id: :desc).to_sql,"DESC" + end + def test_raising_exception_on_invalid_hash_params - assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) } + e = assert_raise(ArgumentError) { Topic.order(:name, "id DESC", id: :asfsdf) } + assert_equal 'Direction "asfsdf" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]', e.message end def test_finding_last_with_arel_order @@ -338,6 +366,60 @@ class RelationTest < ActiveRecord::TestCase assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) end + def test_null_relation_sum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + end + + def test_null_relation_count + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).count + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).count + assert_equal 0, ac.engines.count + end + + def test_null_relation_size + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + ac.save + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + end + + def test_null_relation_average + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + end + + def test_null_relation_minimum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + end + + def test_null_relation_maximum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -546,6 +628,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal expected, actual end + def test_to_sql_on_scoped_proxy + auth = Author.first + Post.where("1=1").written_by(auth) + assert_not auth.posts.to_sql.include?("1=1") + end + def test_loading_with_one_association_with_non_preload posts = Post.eager_load(:last_comment).order('comments.id DESC') post = posts.find { |p| p.id == 1 } @@ -610,7 +698,7 @@ class RelationTest < ActiveRecord::TestCase def test_find_with_list_of_ar author = Author.first - authors = Author.find([author]) + authors = Author.find([author.id]) assert_equal author, authors.first end @@ -655,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], relation.to_a end + def test_typecasting_where_with_array + ids = Author.pluck(:id) + slugs = ids.map { |id| "#{id}-as-a-slug" } + + assert_equal Author.all.to_a, Author.where(id: slugs).to_a + end + def test_find_all_using_where_with_relation david = authors(:david) # switching the lines below would succeed in current rails @@ -742,7 +837,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(authors(:mary).id) assert ! davids.exists?("42") assert ! davids.exists?(42) - assert ! davids.exists?(davids.new) + assert ! davids.exists?(davids.new.id) fake = Author.where(:name => 'fake author') assert ! fake.exists? @@ -787,8 +882,22 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end - def test_delete_all_limit_error + def test_delete_all_with_unpermitted_relation_raises_error assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } + end + + def test_select_with_aggregates + posts = Post.select(:title, :body) + + assert_equal 11, posts.count(:all) + assert_equal 11, posts.size + assert posts.any? + assert posts.many? + assert_not posts.empty? end def test_select_takes_a_variable_list_of_args @@ -821,6 +930,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal 9, posts.where(:comments_count => 0).count end + def test_count_on_association_relation + author = Author.last + another_author = Author.first + posts = Post.where(author_id: author.id) + + assert_equal author.posts.where(author_id: author.id).size, posts.count + + assert_equal 0, author.posts.where(author_id: another_author.id).size + assert author.posts.where(author_id: another_author.id).empty? + end + def test_count_with_distinct posts = Post.all @@ -831,6 +951,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.distinct(false).select(:comments_count).count end + def test_update_all_with_scope + tag = Tag.first + Post.tagged_with(tag.id).update_all title: "rofl" + list = Post.tagged_with(tag.id).all.to_a + assert_operator list.length, :>, 0 + list.each { |post| assert_equal 'rofl', post.title } + end + def test_count_explicit_columns Post.update_all(:comments_count => nil) posts = Post.all @@ -1339,6 +1467,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['comments'], scope.references_values end + def test_automatically_added_where_not_references + scope = Post.where.not(comments: { body: "Bla" }) + assert_equal ['comments'], scope.references_values + + scope = Post.where.not('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 @@ -1383,6 +1519,18 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], scope.references_values end + def test_order_with_reorder_nil_removes_the_order + relation = Post.order(:title).reorder(nil) + + assert_nil relation.order_values.first + end + + def test_reverse_order_with_reorder_nil_removes_the_order + relation = Post.order(:title).reverse_order.reorder(nil) + + assert_nil relation.order_values.first + end + def test_presence topics = Topic.all @@ -1558,10 +1706,8 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) + left = Post.where(id: 20) + right = Post.where(id: [1,2,3,4]) merged = left.merge(right) assert_equal [], merged.bind_values @@ -1571,8 +1717,7 @@ class RelationTest < ActiveRecord::TestCase column = Post.columns_hash['id'] binds = [[column, 20]] - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds + right = Post.where(id: 20) left = Post.where(id: 10) merged = left.merge(right) @@ -1580,19 +1725,15 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] + post = Post.first + right = Post.where(id: post.id) + left = Post.where(title: post.title) merged = left.merge(right) assert_equal post, merged.first end + + def test_relation_join_method + assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",") + end end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..d6decafad9 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,7 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +18,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +32,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 954eab8022..dca85fb5eb 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -51,4 +51,31 @@ class SanitizeTest < ActiveRecord::TestCase select_author_sql = Post.send(:sanitize_sql_array, ['']) assert_equal('', select_author_sql) end + + def test_sanitize_sql_like + assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%') + assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string') + assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42') + end + + def test_sanitize_sql_like_with_custom_escape_character + assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!') + assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!') + assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!') + assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!') + end + + def test_sanitize_sql_like_example_use_case + searchable_post = Class.new(Post) do + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term, '!')) + end + end + + assert_sql(/LIKE '20!% !_reduction!_!!'/) do + searchable_post.search("20% _reduction_!").to_a + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index c085663efb..4e71d04bc0 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,10 +1,9 @@ require "cases/helper" +require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase - def setup - super + setup do ActiveRecord::SchemaMigration.create_table - @stream = StringIO.new end def standard_dump @@ -25,7 +24,8 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + output = standard_dump + assert_match "# encoding: #{@stream.external_encoding.name}", output end def test_schema_dump @@ -63,7 +63,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean)\s+"/) + if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/) match[0].length end end @@ -177,7 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition @@ -188,7 +188,7 @@ class SchemaDumperTest < ActiveRecord::TestCase index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition @@ -319,6 +319,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_citext_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_citext"} =~ output + assert_match %r[t.citext "text_citext"], output + end + end + def test_schema_dump_includes_ltrees_shorthand_definition output = standard_dump if %r{create_table "postgresql_ltrees"} =~ output @@ -346,9 +353,9 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers if current_adapter?(:OracleAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output + assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output else - assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output + assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output end end @@ -365,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + end + class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -387,13 +407,45 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_no_match %r{create_table "foo_.+_bar"}, output - assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end +end + +class SchemaDumperDefaultsTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :defaults, force: true do |t| + t.string :string_with_default, default: "Hello!" + t.date :date_with_default, default: '2014-06-05' + t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" + t.time :time_with_default, default: "07:17:04" + end + end + + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS defaults' + end + + def test_schema_dump_defaults_with_universally_supported_types + output = dump_table_schema('defaults') + assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output + assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output + assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output + assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output + end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 170e9a49eb..9a4d8c6740 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -395,4 +395,22 @@ class DefaultScopingTest < ActiveRecord::TestCase threads.each(&:join) end end + + test "additional conditions are ANDed with the default scope" do + scope = DeveloperCalledJamis.where(name: "David") + assert_equal 2, scope.where_values.length + assert_equal [], scope.to_a + end + + test "additional conditions in a scope are ANDed with the default scope" do + scope = DeveloperCalledJamis.david + assert_equal 2, scope.where_values.length + assert_equal [], scope.to_a + end + + test "a scope can remove the condition from the default scope" do + scope = DeveloperCalledJamis.david2 + assert_equal 1, scope.where_values.length + assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id) + end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index f0ad9ebb8a..59ec2dd6a4 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -291,6 +291,9 @@ class NamedScopingTest < ActiveRecord::TestCase :relation, # private class method on AR::Base :new, # redefined class method on AR::Base :all, # a default scope + :public, + :protected, + :private ] non_conflicts = [ diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 0018fc06f2..8e512e118a 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -11,6 +11,10 @@ require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + setup do + developers(:david) + end + def test_reverse_order assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end @@ -192,8 +196,9 @@ class NestedRelationScopingTest < ActiveRecord::TestCase Developer.where('salary = 80000').scoping do Developer.limit(10).scoping do devs = Developer.all - assert_match '(salary = 80000)', devs.to_sql - assert_equal 10, devs.taken + sql = devs.to_sql + assert_match '(salary = 80000)', sql + assert_match 'LIMIT 10', sql end end end @@ -259,7 +264,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase end end -class HasManyScopingTest< ActiveRecord::TestCase +class HasManyScopingTest < ActiveRecord::TestCase fixtures :comments, :posts, :people, :references def setup @@ -305,7 +310,7 @@ class HasManyScopingTest< ActiveRecord::TestCase end end -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase +class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase fixtures :posts, :categories, :categories_posts def setup diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index c46060a646..3f52e80e11 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,8 +1,11 @@ require "cases/helper" require 'models/contact' require 'models/topic' +require 'models/book' class SerializationTest < ActiveRecord::TestCase + fixtures :books + FORMATS = [ :xml, :json ] def setup @@ -65,4 +68,28 @@ class SerializationTest < ActiveRecord::TestCase ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end + + def test_read_attribute_for_serialization_with_format_without_method_missing + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new + assert_nil book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_init + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new(format: 'paperback') + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_find + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.find(books(:awdr).id) + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index bc67da8d27..f8d87a3661 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -3,20 +3,29 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/traffic_light' +require 'models/post' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :posts MyObject = Struct.new :attribute1, :attribute2 - def teardown - super + teardown do Topic.serialize("content") end + def test_serialize_does_not_eagerly_load_columns + assert_no_queries do + Topic.reset_column_information + Topic.serialize(:content) + end + end + def test_list_of_serialized_attributes - assert_equal %w(content), Topic.serialized_attributes.keys + assert_deprecated do + assert_equal %w(content), Topic.serialized_attributes.keys + end end def test_serialized_attribute @@ -30,12 +39,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end - def test_serialized_attribute_init_with - topic = Topic.allocate - topic.init_with('attributes' => { 'content' => '--- foo' }) - assert_equal 'foo', topic.content - end - def test_serialized_attribute_in_base_class Topic.serialize("content", Hash) @@ -47,34 +50,56 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(hash, important_topic.content) end - # This test was added to fix GH #4004. Obviously the value returned - # is not really the value 'before type cast' so we should maybe think - # about changing that in the future. - def test_serialized_attribute_before_type_cast_returns_unserialized_value + def test_serialized_attributes_from_database_on_subclass Topic.serialize :content, Hash - t = Topic.new(content: { foo: :bar }) - assert_equal({ foo: :bar }, t.content_before_type_cast) + t = Reply.new(content: { foo: :bar }) + assert_equal({ foo: :bar }, t.content) t.save! - t.reload - assert_equal({ foo: :bar }, t.content_before_type_cast) + t = Reply.last + assert_equal({ foo: :bar }, t.content) end - def test_serialized_attributes_before_type_cast_returns_unserialized_value - Topic.serialize :content, Hash + def test_serialized_attribute_calling_dup_method + Topic.serialize :content, JSON - t = Topic.new(content: { foo: :bar }) - assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"]) + orig = Topic.new(content: { foo: :bar }) + clone = orig.dup + assert_equal(orig.content, clone.content) + end + + def test_serialized_json_attribute_returns_unserialized_value + Topic.serialize :content, JSON + my_post = posts(:welcome) + + t = Topic.new(content: my_post) t.save! t.reload - assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"]) + + assert_instance_of(Hash, t.content) + assert_equal(my_post.id, t.content["id"]) + assert_equal(my_post.title, t.content["title"]) end - def test_serialized_attribute_calling_dup_method + def test_json_read_legacy_null Topic.serialize :content, JSON - t = Topic.new(:content => { :foo => :bar }).dup - assert_equal({ :foo => :bar }, t.content_before_type_cast) + # Force a row to have a JSON "null" instead of a database NULL (this is how + # null values are saved on 4.1 and before) + id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')" + t = Topic.find(id) + + assert_nil t.content + end + + def test_json_read_db_null + Topic.serialize :content, JSON + + # Force a row to have a database NULL instead of a JSON "null" + id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)" + t = Topic.find(id) + + assert_nil t.content end def test_serialized_attribute_declared_in_subclass @@ -117,8 +142,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type Topic.serialize(:content, Hash) - topic = Topic.new(:content => "string") - assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save } + assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic = Topic.new(content: 'string') + topic.save + end end def test_should_raise_exception_on_serialized_attribute_with_type_mismatch @@ -169,45 +196,22 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_with_coder - coder = Class.new { - # Identity - def load(thing) - thing - end - - # base 64 - def dump(thing) - [thing].pack('m') + some_class = Struct.new(:foo) do + def self.dump(value) + value.foo end - }.new - Topic.serialize(:content, coder) - s = 'hello world' - topic = Topic.new(:content => s) - assert topic.save - topic = topic.reload - assert_equal [s].pack('m'), topic.content - end - - def test_serialize_with_bcrypt_coder - crypt_coder = Class.new { - def load(thing) - return unless thing - BCrypt::Password.new thing + def self.load(value) + new(value) end + end - def dump(thing) - BCrypt::Password.create(thing).to_s - end - }.new - - Topic.serialize(:content, crypt_coder) - password = 'password' - topic = Topic.new(:content => password) - assert topic.save - topic = topic.reload - assert_kind_of BCrypt::Password, topic.content - assert_equal(true, topic.content == password, 'password should equal') + Topic.serialize(:content, some_class) + topic = Topic.new(:content => some_class.new('my value')) + topic.save! + topic.reload + assert_kind_of some_class, topic.content + assert_equal topic.content, some_class.new('my value') end def test_serialize_attribute_via_select_method_when_time_zone_available @@ -236,13 +240,19 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.long_state end - def test_serialized_column_should_not_be_wrapped_twice - Topic.serialize(:content, MyObject) + def test_serialized_column_should_unserialize_after_update_column + t = Topic.create(content: "first") + assert_equal("first", t.content) - myobj = MyObject.new('value1', 'value2') - Topic.create(content: myobj) - Topic.create(content: myobj) - type = Topic.column_types["content"] - assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) + t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) + assert_equal("second", t.content) + end + + def test_serialized_column_should_unserialize_after_update_attribute + t = Topic.create(content: "first") + assert_equal("first", t.content) + + t.update_attribute(:content, "second") + assert_equal("second", t.content) end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index 76da49707f..a704b861cb 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -10,27 +10,61 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end + #Cache v 1.1 tests + def test_statement_cache + Book.create(name: "my book") + Book.create(name: "my other book") + + cache = StatementCache.create(Book.connection) do |params| + Book.where(:name => params.bind) + end + + b = cache.execute([ "my book" ], Book, Book.connection) + assert_equal "my book", b[0].name + b = cache.execute([ "my other book" ], Book, Book.connection) + assert_equal "my other book", b[0].name + end + + + def test_statement_cache_id + b1 = Book.create(name: "my book") + b2 = Book.create(name: "my other book") + + cache = StatementCache.create(Book.connection) do |params| + Book.where(id: params.bind) + end + + b = cache.execute([ b1.id ], Book, Book.connection) + assert_equal b1.name, b[0].name + b = cache.execute([ b2.id ], Book, Book.connection) + assert_equal b2.name, b[0].name + end + + def test_find_or_create_by + Book.create(name: "my book") + + a = Book.find_or_create_by(name: "my book") + b = Book.find_or_create_by(name: "my other book") + + assert_equal("my book", a.name) + assert_equal("my other book", b.name) + end + + #End + def test_statement_cache_with_simple_statement - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book").where("author_id > 3") end Book.create(name: "my book", author_id: 4) - books = cache.execute + books = cache.execute([], Book, Book.connection) assert_equal "my book", books[0].name end - def test_statement_cache_with_nil_statement_raises_error - assert_raise(ArgumentError) do - ActiveRecord::StatementCache.new do - nil - end - end - end - def test_statement_cache_with_complex_statement - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton') end @@ -38,12 +72,12 @@ module ActiveRecord molecule = salty.molecules.create(name: 'dioxane') molecule.electrons.create(name: 'lepton') - liquids = cache.execute + liquids = cache.execute([], Book, Book.connection) assert_equal "salty", liquids[0].name end def test_statement_cache_values_differ - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book") end @@ -51,13 +85,13 @@ module ActiveRecord Book.create(name: "my book") end - first_books = cache.execute + first_books = cache.execute([], Book, Book.connection) 3.times do Book.create(name: "my book") end - additional_books = cache.execute + additional_books = cache.execute([], Book, Book.connection) assert first_books != additional_books end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 978cee9cfb..e9cdf94c99 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -163,22 +163,24 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:width, :height], second_model.stored_attributes[:data] end - test "YAML coder initializes the store when a Nil value is given" do - assert_equal({}, @john.params) - end + test "stored_attributes are tracked per subclass" do + first_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :color + end + second_model = Class.new(first_model) do + store_accessor :data, :width, :height + end + third_model = Class.new(first_model) do + store_accessor :data, :area, :volume + end - test "attributes_for_coder should return stored fields already serialized" do - attributes = { - "id" => @john.id, - "name"=> @john.name, - "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n", - "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n", - "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}", - "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n", - "account_id"=> @john.account_id - } + assert_equal [:color], first_model.stored_attributes[:data] + assert_equal [:color, :width, :height], second_model.stored_attributes[:data] + assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + end - assert_equal attributes, @john.attributes_for_coder + test "YAML coder initializes the store when a Nil value is given" do + assert_equal({}, @john.params) end test "dump, load and dump again a model" do @@ -187,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase assert_equal @john, loaded second_dump = YAML.dump(loaded) - assert_equal dumped, second_dump assert_equal @john, YAML.load(second_dump) end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index bf9e14fa4f..01d373b691 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -205,7 +205,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_ip + def test_drops_configurations_with_local_ip @configurations[:development].merge!('host' => '127.0.0.1') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -213,7 +213,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_host + def test_drops_configurations_with_local_host @configurations[:development].merge!('host' => 'localhost') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -221,7 +221,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_blank_hosts + def test_drops_configurations_with_blank_hosts @configurations[:development].merge!('host' => nil) ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -241,7 +241,7 @@ module ActiveRecord ActiveRecord::Base.stubs(:configurations).returns(@configurations) end - def test_creates_current_environment_database + def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'prod-db') @@ -273,6 +273,19 @@ module ActiveRecord end end + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + def test_migrate_receives_correct_env_vars + verbose, version = ENV['VERBOSE'], ENV['VERSION'] + + ENV['VERBOSE'] = 'false' + ENV['VERSION'] = '4' + + ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Tasks::DatabaseTasks.migrate + ensure + ENV['VERBOSE'], ENV['VERSION'] = verbose, version + end + end class DatabaseTasksPurgeTest < ActiveRecord::TestCase include DatabaseTasksSetupper @@ -285,6 +298,34 @@ module ActiveRecord end end + class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase + def test_purges_current_environment_database + configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_current('production') + end + end + + class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase + def test_purge_all_local_configurations + configurations = {:development => {'database' => 'my-db'}} + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'my-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 3e3a2828f3..f58535f044 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @@ -196,8 +197,8 @@ module ActiveRecord ActiveRecord::Base.stubs(:establish_connection).returns(true) end - def test_establishes_connection_to_test_database - ActiveRecord::Base.expects(:establish_connection).with(:test) + def test_establishes_connection_to_the_appropriate_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -307,3 +308,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6ea225178f..0d574d071c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup @@ -241,3 +242,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index da3471adf9..750d5e42dc 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'pathname' +if current_adapter?(:SQLite3Adapter) module ActiveRecord class SqliteDBCreateTest < ActiveRecord::TestCase def setup @@ -189,3 +190,4 @@ module ActiveRecord end end end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 4476ce3410..23a170388e 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -10,19 +10,34 @@ module ActiveRecord end def assert_date_from_db(expected, actual, message = nil) - # SybaseAdapter doesn't have a separate column type just for dates, - # so the time is in the string and incorrectly formatted - if current_adapter?(:SybaseAdapter) - assert_equal expected.to_s, actual.to_date.to_s, message - else - assert_equal expected.to_s, actual.to_s, message - end + assert_equal expected.to_s, actual.to_s, message end - def assert_sql(*patterns_to_match) + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + + def capture_sql SQLCounter.clear_log yield - SQLCounter.log_all + SQLCounter.log_all.dup + end + + def assert_sql(*patterns_to_match) + capture_sql { yield } ensure failed_patterns = [] patterns_to_match.each do |pattern| diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 8195e8a08a..abf6becc17 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'support/ddl_helper' require 'models/developer' require 'models/owner' require 'models/pet' @@ -9,26 +10,11 @@ require 'models/task' class TimestampTest < ActiveRecord::TestCase fixtures :developers, :owners, :pets, :toys, :cars, :tasks - # Model with timestamp attribute accessors, but without timestamps columns. - class TimestampAttributePost < ActiveRecord::Base - attr_accessor :created_at, :updated_at - end - - def setup @developer = Developer.first @owner = Owner.first @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at - - @connection = ActiveRecord::Base.connection - @connection.create_table 'timestamp_attribute_posts', :force => true do |t| - # Must have no timestamps - end - end - - def teardown - @connection.execute 'drop table if exists timestamp_attribute_posts' end def test_saving_a_changed_record_updates_its_timestamp @@ -104,6 +90,18 @@ class TimestampTest < ActiveRecord::TestCase assert_in_delta Time.now, task.ending, 1 end + def test_touching_many_attributes_updates_them + task = Task.first + previous_starting = task.starting + previous_ending = task.ending + task.touch(:starting, :ending) + + assert_not_equal previous_starting, task.starting + assert_not_equal previous_ending, task.ending + assert_in_delta Time.now, task.starting, 1 + assert_in_delta Time.now, task.ending, 1 + end + def test_touching_a_record_without_timestamps_is_unexceptional assert_nothing_raised { Car.first.touch } end @@ -155,6 +153,25 @@ class TimestampTest < ActiveRecord::TestCase assert !@developer.no_touching? end + def test_no_touching_with_callbacks + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers" + + attr_accessor :after_touch_called + + after_touch do |user| + user.after_touch_called = true + end + end + + developer = klass.first + + klass.no_touching do + developer.touch + assert_not developer.after_touch_called + end + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner @@ -365,6 +382,19 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal time, pet.updated_at end + def test_timestamp_column_values_are_present_in_the_callbacks + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + + before_create do + self.born_at = self.created_at + end + end + + person = klass.create first_name: 'David' + assert_not_equal person.born_at, nil + end + def test_timestamp_attributes_for_create toy = Toy.first assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) @@ -394,11 +424,22 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end +end + +class TimestampsWithoutTransactionTest < ActiveRecord::TestCase + include DdlHelper + self.use_transactional_fixtures = false + + class TimestampAttributePost < ActiveRecord::Base + attr_accessor :created_at, :updated_at + end def test_do_not_write_timestamps_on_save_if_they_are_not_attributes - post = TimestampAttributePost.new - assert_nothing_raised ActiveModel::MissingAttributeError do - post.run_callbacks :save + with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do + post = TimestampAttributePost.new(id: 1) + post.save! # should not try to assign and persist created_at, updated_at + assert_nil post.created_at + assert_nil post.updated_at end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 1664f1a096..b4849222b8 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -5,6 +5,7 @@ require 'models/developer' require 'models/book' require 'models/author' require 'models/post' +require 'models/movie' class TransactionTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -14,6 +15,11 @@ class TransactionTest < ActiveRecord::TestCase @first, @second = Topic.find(1, 2).sort_by { |t| t.id } end + def test_persisted_in_a_model_with_custom_primary_key_after_failed_save + movie = Movie.create + assert !movie.persisted? + end + def test_raise_after_destroy assert_not @first.frozen? @@ -117,6 +123,19 @@ class TransactionTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_rolling_back_in_a_callback_rollbacks_before_save + def @first.before_save_for_transaction + raise ActiveRecord::Rollback + end + assert !@first.approved + + Topic.transaction do + @first.approved = true + @first.save! + end + assert !Topic.find(@first.id).approved?, "Should not commit the approved flag" + end + def test_raising_exception_in_nested_transaction_restore_state_in_save topic = Topic.new @@ -405,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_savepoints_name + Topic.transaction do + assert_nil Topic.connection.current_savepoint_name + assert_nil Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_2", Topic.connection.current_savepoint_name + assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name + end + + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') @@ -507,13 +546,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_rollback connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_rollback + transaction.rollback assert transaction.state.rolledback? assert !transaction.state.committed? @@ -521,13 +560,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_commit connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_commit + transaction.commit assert !transaction.state.rolledback? assert transaction.state.committed? diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb new file mode 100644 index 0000000000..da30de373e --- /dev/null +++ b/activerecord/test/cases/type/decimal_test.rb @@ -0,0 +1,38 @@ +require "cases/helper" + +module ActiveRecord + module Type + class DecimalTest < ActiveRecord::TestCase + def test_type_cast_decimal + type = Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + end + + def test_type_cast_decimal_from_float_with_large_precision + type = Decimal.new(precision: ::Float::DIG + 2) + assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) + end + + def test_type_cast_decimal_from_rational_with_precision + type = Decimal.new(precision: 2) + assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 + type = Decimal.new + assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_object_responding_to_d + value = Object.new + def value.to_d + BigDecimal.new("1") + end + type = Decimal.new + assert_equal BigDecimal("1"), type.type_cast_from_user(value) + end + end + end +end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb new file mode 100644 index 0000000000..420177ed49 --- /dev/null +++ b/activerecord/test/cases/type/string_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +module ActiveRecord + class StringTypeTest < ActiveRecord::TestCase + test "type casting" do + type = Type::String.new + assert_equal "1", type.type_cast_from_user(true) + assert_equal "0", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.type_cast_from_user(s) + assert_not_same s, type.type_cast_from_database(s) + end + + test "string mutations are detected" do + klass = Class.new(Base) + klass.table_name = 'authors' + + author = klass.create!(name: 'Sean') + assert_not author.changed? + + author.name << ' Griffin' + assert author.name_changed? + + author.save! + author.reload + + assert_equal 'Sean Griffin', author.name + assert_not author.changed? + end + end +end diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb new file mode 100644 index 0000000000..4e32f92dd0 --- /dev/null +++ b/activerecord/test/cases/type/type_map_test.rb @@ -0,0 +1,130 @@ +require "cases/helper" + +module ActiveRecord + module Type + class TypeMapTest < ActiveRecord::TestCase + def test_default_type + mapping = TypeMap.new + + assert_kind_of Value, mapping.lookup(:undefined) + end + + def test_registering_types + boolean = Boolean.new + mapping = TypeMap.new + + mapping.register_type(/boolean/i, boolean) + + assert_equal mapping.lookup('boolean'), boolean + end + + def test_overriding_registered_types + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/time/i, time) + mapping.register_type(/time/i, timestamp) + + assert_equal mapping.lookup('time'), timestamp + end + + def test_fuzzy_lookup + string = String.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i, string) + + assert_equal mapping.lookup('varchar(20)'), string + end + + def test_aliasing_types + string = String.new + mapping = TypeMap.new + + mapping.register_type(/string/i, string) + mapping.alias_type(/varchar/i, 'string') + + assert_equal mapping.lookup('varchar'), string + end + + def test_changing_type_changes_aliases + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/timestamp/i, time) + mapping.alias_type(/datetime/i, 'timestamp') + mapping.register_type(/timestamp/i, timestamp) + + assert_equal mapping.lookup('datetime'), timestamp + end + + def test_aliases_keep_metadata + mapping = TypeMap.new + + mapping.register_type(/decimal/i) { |sql_type| sql_type } + mapping.alias_type(/number/i, 'decimal') + + assert_equal mapping.lookup('number(20)'), 'decimal(20)' + assert_equal mapping.lookup('number'), 'decimal' + end + + def test_register_proc + string = String.new + binary = Binary.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type| + if type.include?('(') + string + else + binary + end + end + + assert_equal mapping.lookup('varchar(20)'), string + assert_equal mapping.lookup('varchar'), binary + end + + def test_additional_lookup_args + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type, limit| + if limit > 255 + 'text' + else + 'string' + end + end + mapping.alias_type(/string/i, 'varchar') + + assert_equal mapping.lookup('varchar', 200), 'string' + assert_equal mapping.lookup('varchar', 400), 'text' + assert_equal mapping.lookup('string', 400), 'text' + end + + def test_requires_value_or_block + mapping = TypeMap.new + + assert_raises(ArgumentError) do + mapping.register_type(/only key/i) + end + end + + def test_lookup_non_strings + mapping = HashLookupTypeMap.new + + mapping.register_type(1, 'string') + mapping.register_type(2, 'int') + mapping.alias_type(3, 1) + + assert_equal mapping.lookup(1), 'string' + assert_equal mapping.lookup(2), 'int' + assert_equal mapping.lookup(3), 'string' + assert_kind_of Type::Value, mapping.lookup(4) + end + end + end +end + diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb new file mode 100644 index 0000000000..5c54812f30 --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,163 @@ +require "cases/helper" +require 'models/company' + +module ActiveRecord + module ConnectionAdapters + class TypesTest < ActiveRecord::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.type_cast_from_user('').nil? + assert type.type_cast_from_user(nil).nil? + + assert type.type_cast_from_user(true) + assert type.type_cast_from_user(1) + assert type.type_cast_from_user('1') + assert type.type_cast_from_user('t') + assert type.type_cast_from_user('T') + assert type.type_cast_from_user('true') + assert type.type_cast_from_user('TRUE') + assert type.type_cast_from_user('on') + assert type.type_cast_from_user('ON') + + # explicitly check for false vs nil + assert_equal false, type.type_cast_from_user(false) + assert_equal false, type.type_cast_from_user(0) + assert_equal false, type.type_cast_from_user('0') + assert_equal false, type.type_cast_from_user('f') + assert_equal false, type.type_cast_from_user('F') + assert_equal false, type.type_cast_from_user('false') + assert_equal false, type.type_cast_from_user('FALSE') + assert_equal false, type.type_cast_from_user('off') + assert_equal false, type.type_cast_from_user('OFF') + assert_equal false, type.type_cast_from_user(' ') + assert_equal false, type.type_cast_from_user("\u3000\r\n") + assert_equal false, type.type_cast_from_user("\u0000") + assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') + end + + def test_type_cast_integer + type = Type::Integer.new + assert_equal 1, type.type_cast_from_user(1) + assert_equal 1, type.type_cast_from_user('1') + assert_equal 1, type.type_cast_from_user('1ignore') + assert_equal 0, type.type_cast_from_user('bad1') + assert_equal 0, type.type_cast_from_user('bad') + assert_equal 1, type.type_cast_from_user(1.7) + assert_equal 0, type.type_cast_from_user(false) + assert_equal 1, type.type_cast_from_user(true) + assert_nil type.type_cast_from_user(nil) + end + + def test_type_cast_non_integer_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user([1,2]) + assert_nil type.type_cast_from_user({1 => 2}) + assert_nil type.type_cast_from_user((1..2)) + end + + def test_type_cast_activerecord_to_integer + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast_from_user(firm) + end + + def test_type_cast_object_without_to_i_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user(Object.new) + end + + def test_type_cast_nan_and_infinity_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user(Float::NAN) + assert_nil type.type_cast_from_user(1.0/0.0) + end + + def test_changing_integers + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.type_cast_from_user("1") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal "1", type.type_cast_from_user("1") + assert_equal 1, type.type_cast_from_user(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.type_cast_from_user(30.minutes) + assert_equal 7200, type.type_cast_from_user(2.hours) + end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + if current_adapter?(:SQLite3Adapter) + def test_binary_encoding + type = SQLite3Binary.new + utf8_string = "a string".encode(Encoding::UTF_8) + type_cast = type.type_cast_from_user(utf8_string) + + assert_equal Encoding::ASCII_8BIT, type_cast.encoding + end + end + end + end +end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index e82ca3f93d..afb893a52c 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -11,7 +11,7 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase @specification = ActiveRecord::Base.remove_connection end - def teardown + teardown do @underlying = nil ActiveRecord::Base.establish_connection(@specification) load_schema if in_memory_db? diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 602f633c45..e4edc437e6 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -1,44 +1,14 @@ -# encoding: utf-8 require "cases/helper" require 'models/topic' require 'models/reply' -require 'models/owner' -require 'models/pet' require 'models/man' require 'models/interest' class AssociationValidationTest < ActiveRecord::TestCase - fixtures :topics, :owners + fixtures :topics repair_validations(Topic, Reply) - def test_validates_size_of_association - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'apet') - assert o.valid? - end - end - - def test_validates_size_of_association_using_within - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - - o.pets.build('name' => 'apet') - assert o.valid? - - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors[:pets].any? - end - end - def test_validates_associated_many Topic.validates_associated(:replies) Reply.validates_presence_of(:content) @@ -94,17 +64,6 @@ class AssociationValidationTest < ActiveRecord::TestCase assert r.valid? end - def test_validates_size_of_association_utf8 - repair_validations Owner 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 - end - def test_validates_presence_of_belongs_to_association__parent_is_new_record repair_validations(Interest) do # Note that Interest and Man have the :inverse_of option set diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index efa0c9b934..268d7914b5 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase repair_validations(Topic, Reply) def setup + repair_validations(Topic, Reply) Reply.validates_presence_of(:title) @topic = Topic.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend @@ -14,7 +15,7 @@ class I18nValidationTest < ActiveRecord::TestCase I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}}) end - def teardown + teardown do I18n.load_path.replace @old_load_path I18n.backend = @old_backend end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb new file mode 100644 index 0000000000..4a92da38ce --- /dev/null +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'models/owner' +require 'models/pet' + +class LengthValidationTest < ActiveRecord::TestCase + fixtures :owners + repair_validations(Owner) + + def test_validates_size_of_association + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'apet') + assert o.valid? + end + end + + def test_validates_size_of_association_using_within + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + + o.pets.build('name' => 'apet') + assert o.valid? + + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? + end + end + + def test_validates_size_of_association_utf8 + repair_validations Owner 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 + end +end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3790d3c8cf..4f38849131 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase end def test_validates_presence_doesnt_convert_to_array - Speedometer.validates_presence_of :dashboard + speedometer = Class.new(Speedometer) + speedometer.validates_presence_of :dashboard dash = Dashboard.new # dashboard has to_a method def dash.to_a; ['(/)', '(\)']; end - s = Speedometer.new + s = speedometer.new s.dashboard = dash assert_nothing_raised { s.valid? } diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 74c696c858..18221cc73d 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -223,7 +223,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t_utf8.save, "Should save t_utf8 as unique" # If database hasn't UTF-8 character set, this test fails - if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!" + if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8.id).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" diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index c02b3241cd..2bbf0f23b3 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -13,7 +13,7 @@ module ActiveRecord end def repair_validations(*model_classes) - yield + yield if block_given? ensure model_classes.each do |k| k.clear_validators! diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index de618902aa..55804f9576 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -14,28 +14,28 @@ class ValidationsTest < ActiveRecord::TestCase # Other classes we mess with will be dealt with in the specific tests repair_validations(Topic) - def test_error_on_create + def test_valid_uses_create_context_when_new r = WrongReply.new r.title = "Wrong Create" - assert !r.save + assert_not r.valid? assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error" end - def test_error_on_update + def test_valid_uses_update_context_when_persisted r = WrongReply.new r.title = "Bad" r.content = "Good" - assert r.save, "First save should be successful" + assert r.save, "First validation should be successful" r.title = "Wrong Update" - assert !r.save, "Second save should fail" + assert_not r.valid?, "Second validation should fail" assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error" end - def test_error_on_given_context + def test_valid_using_special_context r = WrongReply.new(:title => "Valid title") assert !r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join @@ -45,11 +45,26 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) r.author_name = nil - assert !r.save(:context => :special_case) + assert_not r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join r.author_name = "secret" - assert r.save(:context => :special_case) + assert r.valid?(:special_case) + end + + def test_validate + r = WrongReply.new + + r.validate + assert_empty r.errors[:author_name] + + r.validate(:special_case) + assert_not_empty r.errors[:author_name] + + r.author_name = "secret" + + r.validate(:special_case) + assert_empty r.errors[:author_name] end def test_invalid_record_exception @@ -63,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal r, invalid.record end + def test_validate_with_bang + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate! + end + end + + def test_validate_with_bang_and_context + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate!(:special_case) + end + r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good") + assert r.validate!(:special_case) + end + def test_exception_on_create_bang_many assert_raise(ActiveRecord::RecordInvalid) do WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }]) @@ -85,7 +114,7 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_create_without_validation + def test_save_without_validation reply = WrongReply.new assert !reply.save assert reply.save(:validate => false) diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 78fa2f935a..c34e7d5a30 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "rexml/document" require 'models/contact' require 'models/post' require 'models/author' @@ -225,7 +226,6 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema written_on_in_current_timezone = topics(:first).written_on.xmlschema - last_read_in_current_timezone = topics(:first).last_read.xmlschema assert_equal "topic", xml.root.name assert_equal "The First Topic" , xml.elements["//title"].text @@ -247,14 +247,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer", xml.elements["//parent-id"].attributes['type'] assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - if current_adapter?(:SybaseAdapter) - assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] - else - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - end + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) + assert_equal "2004-04-15", xml.elements["//last-read"].text + assert_equal "date" , xml.elements["//last-read"].attributes['type'] # Oracle and DB2 don't have true boolean or time-only fields unless current_adapter?(:OracleAdapter, :DB2Adapter) @@ -421,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase def test_should_support_aliased_attributes xml = Author.select("name as firstname").to_xml - array = Hash.from_xml(xml)['authors'] - assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size + Author.all.each do |author| + assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml + end end def test_array_to_xml_including_has_many_association diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 15815d56e4..bce59b4fcd 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,8 +1,11 @@ require 'cases/helper' require 'models/topic' +require 'models/reply' +require 'models/post' +require 'models/author' class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :authors, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -23,13 +26,6 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content) end - def test_encode_with_coder - topic = Topic.first - coder = {} - topic.encode_with coder - assert_equal({'attributes' => topic.attributes}, coder) - end - def test_psych_roundtrip topic = Topic.first assert topic @@ -47,4 +43,44 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_active_record_relation_serialization [Topic.all].to_yaml end + + def test_raw_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal "123", topic.parent_id_before_type_cast + assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast + end + + def test_cast_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal 123, topic.parent_id + assert_equal 123, YAML.load(YAML.dump(topic)).parent_id + end + + def test_new_records_remain_new_after_round_trip + topic = Topic.new + + assert topic.new_record?, "Sanity check that new records are new" + assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization" + + topic.save! + + assert_not topic.new_record?, "Saved records are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization" + + topic = Topic.select('title').last + + assert_not topic.new_record?, "Loaded records without ID are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization" + end + + def test_types_of_virtual_columns_are_not_changed_on_round_trip + author = Author.select('authors.*, count(posts.id) as posts_count') + .joins(:posts) + .group('authors.id') + .first + dumped = YAML.load(YAML.dump(author)) + + assert_equal 5, author.posts_count + assert_equal 5, dumped.posts_count + end end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 479b8c050d..a54914c372 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -51,28 +51,6 @@ connections: password: arunit database: arunit2 - firebird: - arunit: - host: localhost - username: rails - password: rails - charset: UTF8 - arunit2: - host: localhost - username: rails - password: rails - charset: UTF8 - - frontbase: - arunit: - host: localhost - username: rails - session_name: unittest-<%= $$ %> - arunit2: - host: localhost - username: rails - session_name: unittest-<%= $$ %> - mysql: arunit: username: rails @@ -130,11 +108,3 @@ connections: arunit2: adapter: sqlite3 database: ':memory:' - - sybase: - arunit: - host: database_ASE - username: sa - arunit2: - host: database_ASE - username: sa diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index fb48645456..abe56752c6 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -2,8 +2,10 @@ awdr: author_id: 1 id: 1 name: "Agile Web Development with Rails" + format: "paperback" rfr: author_id: 1 id: 2 name: "Ruby for Rails" + format: "ebook" diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml index daf969d7da..7281a4d768 100644 --- a/activerecord/test/fixtures/computers.yml +++ b/activerecord/test/fixtures/computers.yml @@ -1,4 +1,5 @@ workstation: id: 1 + system: 'Linux' developer: 1 extendedWarranty: 1 diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1
\ No newline at end of file + pk_id: 1
\ No newline at end of file diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml index 6004f390a4..1bb3bf0051 100644 --- a/activerecord/test/fixtures/pirates.yml +++ b/activerecord/test/fixtures/pirates.yml @@ -7,3 +7,6 @@ redbeard: parrot: louis created_on: "<%= 2.weeks.ago.to_s(:db) %>" updated_on: "<%= 2.weeks.ago.to_s(:db) %>" + +mark: + catchphrase: "X $LABELs the spot!" diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 7298096025..86d46f753a 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -4,7 +4,6 @@ welcome: title: Welcome to the weblog body: Such a lovely day comments_count: 2 - taggings_count: 1 tags_count: 1 type: Post @@ -14,7 +13,6 @@ thinking: title: So I was thinking body: Like I hopefully always am comments_count: 1 - taggings_count: 1 tags_count: 1 type: SpecialPost diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index bf049abbf1..4c98b10380 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -6,7 +6,7 @@ first: 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 + content: "--- Have a nice day\n...\n" approved: false replies_count: 1 @@ -15,7 +15,7 @@ second: 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 + content: "--- Have a nice day\n...\n" approved: true replies_count: 0 parent_id: 1 @@ -26,7 +26,7 @@ third: title: The Third Topic of the day author_name: Carl written_on: 2012-08-12t20:24:22.129346+00:00 - content: I'm a troll + content: "--- I'm a troll\n...\n" approved: true replies_count: 1 @@ -35,7 +35,7 @@ fourth: title: The Fourth Topic of the day author_name: Carl written_on: 2006-07-15t15:28:00.0099+01:00 - content: Why not? + content: "--- Why not?\n...\n" approved: true type: Reply parent_id: 3 @@ -45,5 +45,5 @@ fifth: title: The Fifth Topic of the day author_name: Jason written_on: 2013-07-13t12:11:00.0099+01:00 - content: Omakase + content: "--- Omakase\n...\n" approved: true diff --git a/activerecord/test/fixtures/uuid_children.yml b/activerecord/test/fixtures/uuid_children.yml new file mode 100644 index 0000000000..a7b15016e2 --- /dev/null +++ b/activerecord/test/fixtures/uuid_children.yml @@ -0,0 +1,3 @@ +sonny: + uuid_parent: daddy + name: Sonny diff --git a/activerecord/test/fixtures/uuid_parents.yml b/activerecord/test/fixtures/uuid_parents.yml new file mode 100644 index 0000000000..0b40225c5c --- /dev/null +++ b/activerecord/test/fixtures/uuid_parents.yml @@ -0,0 +1,2 @@ +daddy: + name: Daddy diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index c197951c71..8949cf5826 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -140,6 +140,8 @@ class Author < ActiveRecord::Base has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude' has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments + has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" + scope :relation_include_posts, -> { includes(:posts) } scope :relation_include_tags, -> { includes(:tags) } diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 7da39a8e33..272223e1d8 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -22,6 +22,7 @@ class Category < ActiveRecord::Base end has_many :categorizations + has_many :special_categorizations has_many :post_comments, :through => :posts, :source => :comments has_many :authors, :through => :categorizations diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 566e0873f1..6ceafe5858 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -6,9 +6,18 @@ class Club < ActiveRecord::Base has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member" belongs_to :category + has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member + private def private_method "I'm sorry sir, this is a *private* club, not a *pirate* club" end end + +class SuperClub < ActiveRecord::Base + self.table_name = "clubs" + + has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id' + has_many :members, through: :memberships +end diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb index c7495d7deb..501af4a8dd 100644 --- a/activerecord/test/models/college.rb +++ b/activerecord/test/models/college.rb @@ -1,5 +1,10 @@ require_dependency 'models/arunit2_model' +require 'active_support/core_ext/object/with_options' class College < ARUnit2Model has_many :courses + + with_options dependent: :destroy do |assoc| + assoc.has_many :students, -> { where(active: true) } + end end diff --git a/activerecord/test/models/column.rb b/activerecord/test/models/column.rb new file mode 100644 index 0000000000..499358b4cf --- /dev/null +++ b/activerecord/test/models/column.rb @@ -0,0 +1,3 @@ +class Column < ActiveRecord::Base + belongs_to :record +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index ede5fbd0c6..15970758db 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -7,6 +7,9 @@ class Comment < ActiveRecord::Base scope :created, -> { all } belongs_to :post, :counter_cache => true + belongs_to :author, polymorphic: true + belongs_to :resource, polymorphic: true + has_many :ratings belongs_to :first_post, :foreign_key => :post_id @@ -26,6 +29,10 @@ class Comment < ActiveRecord::Base all end scope :all_as_scope, -> { all } + + def to_s + body + end end class SpecialComment < Comment @@ -36,3 +43,11 @@ end class VerySpecialComment < Comment end + +class CommentThatAutomaticallyAltersPostBody < Comment + belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id + + after_save do |comment| + comment.post.update_attributes(body: "Automatically altered") + end +end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 38b0b6aafa..dae102d12b 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -46,6 +46,24 @@ module MyApplication end end end + + module Suffixed + def self.table_name_suffix + '_suffixed' + end + + class Company < ActiveRecord::Base + end + + class Firm < Company + self.table_name = 'companies' + end + + module Nested + class Company < ActiveRecord::Base + end + end + end end module Billing diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index a1cb8d62b6..3ea17c3abf 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -8,6 +8,7 @@ module ContactFakeColumns table_name => 'id' } + column :id, :integer column :name, :string column :age, :integer column :avatar, :binary diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 2e2d8a0d37..5bd2f00129 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -13,6 +13,8 @@ class Developer < ActiveRecord::Base end end + accepts_nested_attributes_for :projects + has_and_belongs_to_many :projects_extended_by_name, -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", @@ -74,12 +76,6 @@ class AuditLog < ActiveRecord::Base belongs_to :unvalidated_developer, :class_name => 'Developer' end -DeveloperSalary = Struct.new(:amount) -class DeveloperWithAggregate < ActiveRecord::Base - self.table_name = 'developers' - composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)] -end - class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base self.table_name = 'developers' has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id' @@ -165,6 +161,8 @@ class DeveloperCalledJamis < ActiveRecord::Base default_scope { where(:name => 'Jamis') } scope :poor, -> { where('salary < 150000') } + scope :david, -> { where name: "David" } + scope :david2, -> { unscoped.where name: "David" } end class PoorDeveloperCalledJamis < ActiveRecord::Base diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index edb75d333f..91e46f83e5 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,6 +1,8 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face + # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index f4d127730c..4fbb6b226b 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,6 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb index c441be2bef..0302abad1e 100644 --- a/activerecord/test/models/movie.rb +++ b/activerecord/test/models/movie.rb @@ -1,3 +1,5 @@ class Movie < ActiveRecord::Base self.primary_key = "movieid" + + validates_presence_of :name end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index cf24502d3a..2e3a9a3681 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + belongs_to :last_pet, class_name: 'Pet' + scope :including_last_pet, -> { + select(%q[ + owners.*, ( + select p.pet_id from pets p + where p.owner_id = owners.owner_id + order by p.name desc + limit 1 + ) as last_pet_id + ]).includes(:last_pet) + } + after_commit :execute_blocks def blocks diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 1a282dbce4..c7e54e7b63 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -89,6 +89,19 @@ class RichPerson < ActiveRecord::Base self.table_name = 'people' has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' + + before_validation :run_before_create, on: :create + before_validation :run_before_validation + + private + + def run_before_create + self.first_name = first_name.to_s + 'run_before_create' + end + + def run_before_validation + self.first_name = first_name.to_s + 'run_before_validation' + end end class NestedPerson < ActiveRecord::Base diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 8510c596a7..90a3c3ecee 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -18,7 +18,6 @@ class Pirate < ActiveRecord::Base has_many :treasures, :as => :looter has_many :treasure_estimates, :through => :treasures, :source => :price_estimates - # These both have :autosave enabled because accepts_nested_attributes_for is used on them. has_one :ship has_one :update_only_ship, :class_name => 'Ship' has_one :non_validated_ship, :class_name => 'Ship' @@ -88,8 +87,6 @@ end class FamousPirate < ActiveRecord::Base self.table_name = 'pirates' - has_many :famous_ships - validates_presence_of :catchphrase, on: :conference -end +end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index faf539a562..a29858213b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -40,6 +40,8 @@ class Post < ActiveRecord::Base scope :with_comments, -> { preload(:comments) } scope :with_tags, -> { preload(:taggings) } + scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) } + has_many :comments do def find_most_recent order("id DESC").first @@ -86,7 +88,7 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - has_many :taggings, :as => :taggable + has_many :taggings, :as => :taggable, :counter_cache => :tags_count has_many :tags, :through => :taggings do def add_joins_and_select select('tags.*, authors.id as author_id') @@ -145,10 +147,18 @@ class Post < ActiveRecord::Base has_many :lazy_readers has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader' + has_many :lazy_people, :through => :lazy_readers, :source => :person + has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader' + has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person + def self.top(limit) ranked_by_comments.limit_by(limit) end + def self.written_by(author) + where(id: author.posts.pluck(:id)) + end + def self.reset_log @log = [] end @@ -157,10 +167,6 @@ class Post < ActiveRecord::Base return @log if message.nil? @log << [message, side, new_record] end - - def self.what_are_you - 'a post...' - end end class SpecialPost < Post; end @@ -202,3 +208,12 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } end + +class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base + self.table_name = 'posts' + has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id + + after_save do |post| + post.comments.load + end +end diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb new file mode 100644 index 0000000000..0d4a7f9235 --- /dev/null +++ b/activerecord/test/models/publisher.rb @@ -0,0 +1,2 @@ +module Publisher +end diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb new file mode 100644 index 0000000000..d73a8eb936 --- /dev/null +++ b/activerecord/test/models/publisher/article.rb @@ -0,0 +1,4 @@ +class Publisher::Article < ActiveRecord::Base + has_and_belongs_to_many :magazines + has_and_belongs_to_many :tags +end diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb new file mode 100644 index 0000000000..82e1a14008 --- /dev/null +++ b/activerecord/test/models/publisher/magazine.rb @@ -0,0 +1,3 @@ +class Publisher::Magazine < ActiveRecord::Base + has_and_belongs_to_many :articles +end diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index 3a6b7fad34..91afc1898c 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -16,6 +16,8 @@ class LazyReader < ActiveRecord::Base self.table_name = "readers" default_scope -> { where(skimmer: true) } + scope :skimmers_or_not, -> { unscope(:where => :skimmer) } + belongs_to :post belongs_to :person end diff --git a/activerecord/test/models/record.rb b/activerecord/test/models/record.rb new file mode 100644 index 0000000000..f77ac9fc03 --- /dev/null +++ b/activerecord/test/models/record.rb @@ -0,0 +1,2 @@ +class Record < ActiveRecord::Base +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 7a369b9d9a..77a4728d0b 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -20,8 +20,6 @@ end class FamousShip < ActiveRecord::Base self.table_name = 'ships' - belongs_to :famous_pirate - validates_presence_of :name, on: :conference end diff --git a/activerecord/test/models/student.rb b/activerecord/test/models/student.rb index f459f2a9a3..28a0b6c99b 100644 --- a/activerecord/test/models/student.rb +++ b/activerecord/test/models/student.rb @@ -1,3 +1,4 @@ class Student < ActiveRecord::Base has_and_belongs_to_many :lessons + belongs_to :college end diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index a581b381e8..80d4725f7e 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -3,5 +3,5 @@ class Tag < ActiveRecord::Base has_many :taggables, :through => :taggings has_one :tagging - has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post' -end
\ No newline at end of file + has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post' +end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f91f2ad2e9..a6c05da26a 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :taggable, :polymorphic => true, :counter_cache => true + belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count has_many :things, :through => :taggable end diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index e864295acf..a69d3fd3df 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -3,6 +3,7 @@ class Treasure < ActiveRecord::Base belongs_to :looter, :polymorphic => true has_many :price_estimates, :as => :estimate_of + has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false accepts_nested_attributes_for :looter end diff --git a/activerecord/test/models/uuid_child.rb b/activerecord/test/models/uuid_child.rb new file mode 100644 index 0000000000..a3d0962ad6 --- /dev/null +++ b/activerecord/test/models/uuid_child.rb @@ -0,0 +1,3 @@ +class UuidChild < ActiveRecord::Base + belongs_to :uuid_parent +end diff --git a/activerecord/test/models/uuid_parent.rb b/activerecord/test/models/uuid_parent.rb new file mode 100644 index 0000000000..5634f22d0c --- /dev/null +++ b/activerecord/test/models/uuid_parent.rb @@ -0,0 +1,3 @@ +class UuidParent < ActiveRecord::Base + has_many :uuid_children +end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index a86a188bcf..e9294a11b9 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,7 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -99,21 +99,23 @@ _SQL _SQL end - if 't' == select_value("select 'json'=ANY(select typname from pg_type)") + if 't' == select_value("select 'citext'=ANY(select typname from pg_type)") execute <<_SQL - CREATE TABLE postgresql_json_data_type ( + CREATE TABLE postgresql_citext ( id SERIAL PRIMARY KEY, - json_data json default '{}'::json + text_citext citext default ''::citext ); _SQL end + if 't' == select_value("select 'json'=ANY(select typname from pg_type)") execute <<_SQL - CREATE TABLE postgresql_moneys ( + CREATE TABLE postgresql_json_data_type ( id SERIAL PRIMARY KEY, - wealth MONEY + json_data json default '{}'::json ); _SQL + end execute <<_SQL CREATE TABLE postgresql_numbers ( @@ -141,14 +143,6 @@ _SQL _SQL execute <<_SQL - CREATE TABLE postgresql_bit_strings ( - id SERIAL PRIMARY KEY, - bit_string BIT(8), - bit_string_varying BIT VARYING(8) - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_oids ( id SERIAL PRIMARY KEY, obj_id OID diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 99a53434f6..a8b21904ac 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -9,13 +9,14 @@ ActiveRecord::Schema.define do #put adapter specific setup here case adapter_name - # For Firebird, set the sequence values 10000 when create_table is called; - # this prevents primary key collisions between "normally" created records - # and fixture-based (YAML) records. - when "Firebird" - def create_table(*args, &block) - ActiveRecord::Base.connection.create_table(*args, &block) - ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000" + when "PostgreSQL" + enable_uuid_ossp!(ActiveRecord::Base.connection) + create_table :uuid_parents, id: :uuid, force: true do |t| + t.string :name + end + create_table :uuid_children, id: :uuid, force: true do |t| + t.string :name + t.uuid :uuid_parent_id end end @@ -53,6 +54,19 @@ ActiveRecord::Schema.define do t.string :name end + create_table :articles, force: true do |t| + end + + create_table :articles_magazines, force: true do |t| + t.references :article + t.references :magazine + end + + create_table :articles_tags, force: true do |t| + t.references :article + t.references :tag + end + create_table :audit_logs, force: true do |t| t.column :message, :string, null: false t.column :developer_id, :integer, null: false @@ -70,6 +84,8 @@ ActiveRecord::Schema.define do create_table :author_addresses, force: true do |t| end + add_foreign_key :authors, :author_addresses + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer @@ -94,6 +110,7 @@ ActiveRecord::Schema.define do create_table :books, force: true do |t| t.integer :author_id + t.string :format t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 @@ -161,6 +178,10 @@ ActiveRecord::Schema.define do t.integer :references, null: false end + create_table :columns, force: true do |t| + t.references :record + end + create_table :comments, force: true do |t| t.integer :post_id, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in @@ -171,9 +192,12 @@ ActiveRecord::Schema.define do t.text :body, null: false end t.string :type - t.integer :taggings_count, default: 0 + t.integer :tags_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id + t.references :author, polymorphic: true + t.string :resource_id + t.string :resource_type end create_table :companies, force: true do |t| @@ -198,6 +222,7 @@ ActiveRecord::Schema.define do end create_table :computers, force: true do |t| + t.string :system t.integer :developer, null: false t.integer :extendedWarranty, null: false end @@ -371,6 +396,9 @@ ActiveRecord::Schema.define do t.column :custom_lock_version, :integer end + create_table :magazines, force: true do |t| + end + create_table :mateys, id: false, force: true do |t| t.column :pirate_id, :integer t.column :target_id, :integer @@ -508,6 +536,7 @@ ActiveRecord::Schema.define do t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 + t.timestamp :born_at t.timestamps end @@ -542,7 +571,6 @@ ActiveRecord::Schema.define do end t.string :type t.integer :comments_count, default: 0 - t.integer :taggings_count, default: 0 t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 @@ -637,6 +665,8 @@ ActiveRecord::Schema.define do create_table :students, force: true do |t| t.string :name + t.boolean :active + t.integer :college_id end create_table :subscribers, force: true, id: false do |t| @@ -670,10 +700,14 @@ ActiveRecord::Schema.define do end create_table :topics, force: true do |t| - t.string :title + t.string :title, limit: 250 t.string :author_name t.string :author_email_address - t.datetime :written_on + if mysql_56? + t.datetime :written_on, limit: 6 + else + t.datetime :written_on + end t.time :bonus_time t.date :last_read # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in @@ -747,6 +781,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type + t.integer :poly_man_without_inverse_id + t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end @@ -812,6 +848,8 @@ ActiveRecord::Schema.define do t.integer :department_id end + create_table :records, force: true do |t| + end except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk @@ -819,12 +857,18 @@ ActiveRecord::Schema.define do t.integer :fk_id, null: false end - create_table :fk_test_has_pk, force: true do |t| + create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| end - execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" + add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" + add_foreign_key :lessons_students, :students + end - execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" + create_table :overloaded_types, force: true do |t| + t.float :overloaded_float, default: 500 + t.float :unoverloaded_float + t.string :overloaded_string_with_limit, limit: 255 + t.string :string_with_default, default: 'the original default' end end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end
\ No newline at end of file +end diff --git a/activerecord/test/support/connection_helper.rb b/activerecord/test/support/connection_helper.rb new file mode 100644 index 0000000000..4a19e5df44 --- /dev/null +++ b/activerecord/test/support/connection_helper.rb @@ -0,0 +1,14 @@ +module ConnectionHelper + def run_without_connection + original_connection = ActiveRecord::Base.remove_connection + yield original_connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + + # Used to drop all cache query plans in tests. + def reset_connection + original_connection = ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(original_connection) + end +end diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb new file mode 100644 index 0000000000..43cb235e01 --- /dev/null +++ b/activerecord/test/support/ddl_helper.rb @@ -0,0 +1,8 @@ +module DdlHelper + def with_example_table(connection, table_name, definition = nil) + connection.execute("CREATE TABLE #{table_name}(#{definition})") + yield + ensure + connection.execute("DROP TABLE #{table_name}") + end +end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb new file mode 100644 index 0000000000..2ae8d299e5 --- /dev/null +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -0,0 +1,11 @@ +module SchemaDumpingHelper + def dump_table_schema(table, connection = ActiveRecord::Base.connection) + old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string + ensure + ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables + end +end |