diff options
Diffstat (limited to 'activerecord')
466 files changed, 19159 insertions, 4491 deletions
diff --git a/activerecord/.gitignore b/activerecord/.gitignore new file mode 100644 index 0000000000..884ee009eb --- /dev/null +++ b/activerecord/.gitignore @@ -0,0 +1,3 @@ +/sqlnet.log +/test/config.yml +/test/fixtures/*.sqlite* diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index cbbfad615d..d1ae41ab97 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,574 +1,77 @@ -* Use `count(:all)` in `HasManyAssociation#count_records` to prevent invalid - SQL queries for association counting. +* Add environment & load_config dependency to `bin/rake db:seed` to enable + seed load in environments without Rails and custom DB configuration - *Klas Eskilson* + *Tobias Bielohlawek* -* Fix to invoke callbacks when using `update_attribute`. +* Fix default value for mysql time types with specified precision. - *Mike Busch* + *Nikolay Kondratyev* -* Fix `count(:all)` to correctly work `distinct` with custom SELECT list. +* Fix `touch` option to behave consistently with `Persistence#touch` method. *Ryuta Kamizono* -* Using subselect for `delete_all` with `limit` or `offset`. +* Migrations raise when duplicate column definition. - *Ryuta Kamizono* - -* Undefine attribute methods on descendants when resetting column - information. - - *Chris Salzberg* - -* Log database query callers - - Add `verbose_query_logs` configuration option to display the caller - of database queries in the log to facilitate N+1 query resolution - and other debugging. - - Enabled in development only for new and upgraded applications. Not - recommended for use in the production environment since it relies - on Ruby's `Kernel#caller_locations` which is fairly slow. - - *Olivier Lacan* + Fixes #33024. -* Fix conflicts `counter_cache` with `touch: true` by optimistic locking. + *Federico Martinez* - ``` - # create_table :posts do |t| - # t.integer :comments_count, default: 0 - # t.integer :lock_version - # t.timestamps - # end - class Post < ApplicationRecord - end +* Bump minimum SQLite version to 3.8 - # create_table :comments do |t| - # t.belongs_to :post - # end - class Comment < ApplicationRecord - belongs_to :post, touch: true, counter_cache: true - end - ``` + *Yasuo Honda* - Before: - ``` - post = Post.create! - # => begin transaction - INSERT INTO "posts" ("created_at", "updated_at", "lock_version") - VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0) - commit transaction +* Fix parent record should not get saved with duplicate children records. - comment = Comment.create!(post: post) - # => begin transaction - INSERT INTO "comments" ("post_id") VALUES (1) + Fixes #32940. - UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1, - "lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1 + *Santosh Wadghule* - UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330', - "lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0 - rollback transaction - # => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post. +* Fix logic on disabling commit callbacks so they are not called unexpectedly when errors occur. - Comment.take.destroy! - # => begin transaction - DELETE FROM "comments" WHERE "comments"."id" = 1 + *Brian Durand* - UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1, - "lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1 +* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?` + use loaded association ids if present. - UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901', - "lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0 - rollback transaction - # => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post. - ``` + *Graham Turner* - After: - ``` - post = Post.create! - # => begin transaction - INSERT INTO "posts" ("created_at", "updated_at", "lock_version") - VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0) - commit transaction +* Add support to preload associations of polymorphic associations when not all the records have the requested associations. - comment = Comment.create!(post: post) - # => begin transaction - INSERT INTO "comments" ("post_id") VALUES (1) + *Dana Sherson* - UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1, - "lock_version" = COALESCE("lock_version", 0) + 1, - "updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1 - commit transaction - - comment.destroy! - # => begin transaction - DELETE FROM "comments" WHERE "comments"."id" = 1 - - UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1, - "lock_version" = COALESCE("lock_version", 0) + 1, - "updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1 - commit transaction - ``` - - Fixes #31199. - - *bogdanvlviv* - -* Add support for PostgreSQL operator classes to `add_index`. +* Add `touch_all` method to `ActiveRecord::Relation`. Example: - add_index :users, :name, using: :gist, opclass: { name: :gist_trgm_ops } - - *Greg Navis* - -* Don't allow scopes to be defined which conflict with instance methods on `Relation`. - - Fixes #31120. - - *kinnrot* - - -## Rails 5.2.0.beta2 (November 28, 2017) ## - -* No changes. - - -## Rails 5.2.0.beta1 (November 27, 2017) ## - -* Add new error class `QueryCanceled` which will be raised - when canceling statement due to user request. - - *Ryuta Kamizono* - -* Add `#up_only` to database migrations for code that is only relevant when - migrating up, e.g. populating a new column. - - *Rich Daley* - -* Require raw SQL fragments to be explicitly marked when used in - relation query methods. - - Before: - ``` - Article.order("LENGTH(title)") - ``` - - After: - ``` - Article.order(Arel.sql("LENGTH(title)")) - ``` - - This prevents SQL injection if applications use the [strongly - discouraged] form `Article.order(params[:my_order])`, under the - mistaken belief that only column names will be accepted. - - Raw SQL strings will now cause a deprecation warning, which will - become an UnknownAttributeReference error in Rails 6.0. Applications - can opt in to the future behavior by setting `allow_unsafe_raw_sql` - to `:disabled`. - - Common and judged-safe string values (such as simple column - references) are unaffected: - ``` - Article.order("title DESC") - ``` - - *Ben Toews* - -* `update_all` will now pass its values to `Type#cast` before passing them to - `Type#serialize`. This means that `update_all(foo: 'true')` will properly - persist a boolean. - - *Sean Griffin* - -* Add new error class `StatementTimeout` which will be raised - when statement timeout exceeded. - - *Ryuta Kamizono* - -* Fix `bin/rails db:migrate` with specified `VERSION`. - `bin/rails db:migrate` with empty VERSION behaves as without `VERSION`. - Check a format of `VERSION`: Allow a migration version number - or name of a migration file. Raise error if format of `VERSION` is invalid. - Raise error if target migration doesn't exist. - - *bogdanvlviv* - -* Fixed a bug where column orders for an index weren't written to - `db/schema.rb` when using the sqlite adapter. - - Fixes #30902. - - *Paul Kuruvilla* - -* Remove deprecated method `#sanitize_conditions`. - - *Rafael Mendonça França* - -* Remove deprecated method `#scope_chain`. - - *Rafael Mendonça França* - -* Remove deprecated configuration `.error_on_ignored_order_or_limit`. - - *Rafael Mendonça França* + Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) -* Remove deprecated arguments from `#verify!`. + *fatkodima*, *duggiefresh* - *Rafael Mendonça França* +* Add `ActiveRecord::Base.base_class?` predicate. -* Remove deprecated argument `name` from `#indexes`. + *Bogdan Gusiev* - *Rafael Mendonça França* +* Add custom prefix/suffix options to `ActiveRecord::Store.store_accessor`. -* Remove deprecated method `ActiveRecord::Migrator.schema_migrations_table_name`. + *Tan Huynh*, *Yukio Mizuta* - *Rafael Mendonça França* +* Rails 6 requires Ruby 2.4.1 or newer. -* Remove deprecated method `supports_primary_key?`. + *Jeremy Daer* - *Rafael Mendonça França* - -* Remove deprecated method `supports_migrations?`. - - *Rafael Mendonça França* - -* Remove deprecated methods `initialize_schema_migrations_table` and `initialize_internal_metadata_table`. - - *Rafael Mendonça França* - -* Raises when calling `lock!` in a dirty record. - - *Rafael Mendonça França* - -* Remove deprecated support to passing a class to `:class_name` on associations. - - *Rafael Mendonça França* - -* Remove deprecated argument `default` from `index_name_exists?`. - - *Rafael Mendonça França* - -* Remove deprecated support to `quoted_id` when typecasting an Active Record object. - - *Rafael Mendonça França* - -* Fix `bin/rails db:setup` and `bin/rails db:test:prepare` create wrong - ar_internal_metadata's data for a test database. - - Before: - ``` - $ RAILS_ENV=test rails dbconsole - > SELECT * FROM ar_internal_metadata; - key|value|created_at|updated_at - environment|development|2017-09-11 23:14:10.815679|2017-09-11 23:14:10.815679 - ``` - - After: - ``` - $ RAILS_ENV=test rails dbconsole - > SELECT * FROM ar_internal_metadata; - key|value|created_at|updated_at - environment|test|2017-09-11 23:14:10.815679|2017-09-11 23:14:10.815679 - ``` - - Fixes #26731. - - *bogdanvlviv* - -* Fix longer sequence name detection for serial columns. - - Fixes #28332. - - *Ryuta Kamizono* - -* MySQL: Don't lose `auto_increment: true` in the `db/schema.rb`. - - Fixes #30894. - - *Ryuta Kamizono* - -* Fix `COUNT(DISTINCT ...)` for `GROUP BY` with `ORDER BY` and `LIMIT`. - - Fixes #30886. - - *Ryuta Kamizono* - -* PostgreSQL `tsrange` now preserves subsecond precision. - - PostgreSQL 9.1+ introduced range types, and Rails added support for using - this datatype in Active Record. However, the serialization of - `PostgreSQL::OID::Range` was incomplete, because it did not properly - cast the bounds that make up the range. This led to subseconds being - dropped in SQL commands: - - Before: - - connection.type_cast(tsrange.serialize(range_value)) - # => "[2010-01-01 13:30:00 UTC,2011-02-02 19:30:00 UTC)" - - Now: - - connection.type_cast(tsrange.serialize(range_value)) - # => "[2010-01-01 13:30:00.670277,2011-02-02 19:30:00.745125)" - - *Thomas Cannon* - -* Passing a `Set` to `Relation#where` now behaves the same as passing an - array. - - *Sean Griffin* - -* Use given algorithm while removing index from database. - - Fixes #24190. - - *Mehmet Emin İNAÇ* - -* Update payload names for `sql.active_record` instrumentation to be - more descriptive. - - Fixes #30586. - - *Jeremy Green* - -* Add new error class `LockWaitTimeout` which will be raised - when lock wait timeout exceeded. - - *Gabriel Courtemanche* - -* Remove deprecated `#migration_keys`. - - *Ryuta Kamizono* +* Deprecate `update_attributes`/`!` in favor of `update`/`!`. -* Automatically guess the inverse associations for STI. + *Eddie Lebow* - *Yuichiro Kaneko* - -* Ensure `sum` honors `distinct` on `has_many :through` associations - - Fixes #16791. - - *Aaron Wortham* - -* Add `binary` fixture helper method. - - *Atsushi Yoshida* - -* When using `Relation#or`, extract the common conditions and put them before the OR condition. - - *Maxime Handfield Lapointe* - -* `Relation#or` now accepts two relations who have different values for - `references` only, as `references` can be implicitly called by `where`. - - Fixes #29411. - - *Sean Griffin* - -* `ApplicationRecord` is no longer generated when generating models. If you - need to generate it, it can be created with `rails g application_record`. - - *Lisa Ugray* - -* Fix `COUNT(DISTINCT ...)` with `ORDER BY` and `LIMIT` to keep the existing select list. - - *Ryuta Kamizono* - -* When a `has_one` association is destroyed by `dependent: destroy`, - `destroyed_by_association` will now be set to the reflection, matching the - behaviour of `has_many` associations. - - *Lisa Ugray* - -* Fix `unscoped(where: [columns])` removing the wrong bind values - - When the `where` is called on a relation after a `or`, unscoping the column of that later `where` removed - bind values used by the `or` instead. (possibly other cases too) - - ``` - Post.where(id: 1).or(Post.where(id: 2)).where(foo: 3).unscope(where: :foo).to_sql - # Currently: - # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2 OR "posts"."id" = 3) - # With fix: - # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 1 OR "posts"."id" = 2) - ``` - - *Maxime Handfield Lapointe* - -* Values constructed using multi-parameter assignment will now use the - post-type-cast value for rendering in single-field form inputs. - - *Sean Griffin* - -* `Relation#joins` is no longer affected by the target model's - `current_scope`, with the exception of `unscoped`. - - Fixes #29338. - - *Sean Griffin* - -* Change sqlite3 boolean serialization to use 1 and 0 - - SQLite natively recognizes 1 and 0 as true and false, but does not natively - recognize 't' and 'f' as was previously serialized. - - This change in serialization requires a migration of stored boolean data - for SQLite databases, so it's implemented behind a configuration flag - whose default false value is deprecated. - - *Lisa Ugray* - -* Skip query caching when working with batches of records (`find_each`, `find_in_batches`, - `in_batches`). - - Previously, records would be fetched in batches, but all records would be retained in memory - until the end of the request or job. - - *Eugene Kenny* - -* Prevent errors raised by `sql.active_record` notification subscribers from being converted into - `ActiveRecord::StatementInvalid` exceptions. - - *Dennis Taylor* - -* Fix eager loading/preloading association with scope including joins. - - Fixes #28324. - - *Ryuta Kamizono* - -* Fix transactions to apply state to child transactions - - Previously, if you had a nested transaction and the outer transaction was rolledback, the record from the - inner transaction would still be marked as persisted. - - This change fixes that by applying the state of the parent transaction to the child transaction when the - parent transaction is rolledback. This will correctly mark records from the inner transaction as not persisted. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Deprecate `set_state` method in `TransactionState` - - Deprecated the `set_state` method in favor of setting the state via specific methods. If you need to mark the - state of the transaction you can now use `rollback!`, `commit!` or `nullify!` instead of - `set_state(:rolledback)`, `set_state(:committed)`, or `set_state(nil)`. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Deprecate delegating to `arel` in `Relation`. - - *Ryuta Kamizono* - -* Fix eager loading to respect `store_full_sti_class` setting. - - *Ryuta Kamizono* - -* Query cache was unavailable when entering the `ActiveRecord::Base.cache` block - without being connected. - - *Tsukasa Oishi* - -* Previously, when building records using a `has_many :through` association, - if the child records were deleted before the parent was saved, they would - still be persisted. Now, if child records are deleted before the parent is saved - on a `has_many :through` association, the child records will not be persisted. - - *Tobias Kraze* - -* Merging two relations representing nested joins no longer transforms the joins of - the merged relation into LEFT OUTER JOIN. Example to clarify: - - ``` - Author.joins(:posts).merge(Post.joins(:comments)) - # Before the change: - #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON... - - # After the change: - #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON... - ``` - - TODO: Add to the Rails 5.2 upgrade guide - - *Maxime Handfield Lapointe* - -* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and - `locking_column`, without default value, is null in the database. - - *bogdanvlviv* - -* Fix destroying existing object does not work well when optimistic locking enabled and - `locking_column` is null in the database. - - *bogdanvlviv* - -* Use bulk INSERT to insert fixtures for better performance. - - *Kir Shatrov* - -* Prevent creation of bind param if casted value is nil. - - *Ryuta Kamizono* - -* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. - - *Ryuta Kamizono* - -* Loading model schema from database is now thread-safe. - - Fixes #28589. - - *Vikrant Chaudhary*, *David Abdemoulaie* - -* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries - in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key - that does not include a timestamp any more. - - NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps - until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ +* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in + ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database. *DHH* -* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump - - *Rusty Geldmacher*, *Guillermo Iguaran* - -* Add type caster to `RuntimeReflection#alias_name` - - Fixes #28959. - - *Jon Moss* - -* Deprecate `supports_statement_cache?`. - - *Ryuta Kamizono* - -* Raise error `UnknownMigrationVersionError` on the movement of migrations - when the current migration does not exist. - - *bogdanvlviv* - -* Fix `bin/rails db:forward` first migration. - - *bogdanvlviv* - -* Support Descending Indexes for MySQL. - - MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored. - See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html. +* Add `Relation#pick` as short-hand for single-value plucks. - *Ryuta Kamizono* - -* Fix inconsistency with changed attributes when overriding AR attribute reader. - - *bogdanvlviv* - -* When calling the dynamic fixture accessor method with no arguments, it now returns all fixtures of this type. - Previously this method always returned an empty array. - - *Kevin McPhillips* + *DHH* -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index cce00cbc3a..04ba107c48 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,5 +1,7 @@ Copyright (c) 2004-2018 David Heinemeier Hansson +Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 591c451da5..170c95b827 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -41,9 +41,11 @@ namespace :test do end end -desc "Build MySQL and PostgreSQL test databases" namespace :db do + desc "Build MySQL and PostgreSQL test databases" task create: ["db:mysql:build", "db:postgresql:build"] + + desc "Drop MySQL and PostgreSQL test databases" task drop: ["db:mysql:drop", "db:postgresql:drop"] end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8e42a11df4..a857d00c05 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Object-relational mapper framework (part of Rails)." s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.4.1" s.license = "MIT" @@ -30,6 +30,4 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "activemodel", version - - s.add_dependency "arel", ">= 9.0" end diff --git a/activerecord/bin/test b/activerecord/bin/test index 83c192531e..9ecf27ce67 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,6 +1,12 @@ #!/usr/bin/env ruby # frozen_string_literal: true +adapter_index = ARGV.index("--adapter") || ARGV.index("-a") +if adapter_index + ARGV.delete_at(adapter_index) + ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip +end + COMPONENT_ROOT = File.expand_path("..", __dir__) require_relative "../../tools/test" @@ -17,4 +23,5 @@ module Minitest end end +Minitest.load_plugins Minitest.extensions.unshift "active_record" diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore deleted file mode 100644 index 0dfc1cb7fb..0000000000 --- a/activerecord/examples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -performance.sql diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index b4377ad6be..d198466dbf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,6 +40,7 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache + autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata @@ -163,6 +164,7 @@ module ActiveRecord "active_record/tasks/postgresql_database_tasks" end + autoload :TestDatabases, "active_record/test_databases" autoload :TestFixtures, "active_record/fixtures" def self.eager_load! diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e5e89734d2..3250e29b82 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -3,8 +3,6 @@ module ActiveRecord # See ActiveRecord::Aggregations::ClassMethods for documentation module Aggregations - extend ActiveSupport::Concern - def initialize_dup(*) # :nodoc: @aggregation_cache = {} super @@ -35,7 +33,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # @@ -177,9 +175,9 @@ module ActiveRecord # # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago": # - # Customer.where(balance: Money.new(20, "USD")) + # Customer.where(address: Address.new("May Street", "Chicago")) # module ClassMethods # Adds reader and writer methods for manipulating a value object: @@ -212,8 +210,7 @@ module ActiveRecord # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) - # composed_of :balance, class_name: "Money", mapping: %w(balance amount), - # converter: Proc.new { |balance| balance.to_money } + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, allow_nil: true @@ -226,6 +223,10 @@ module ActiveRecord def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) + unless self < Aggregations + include Aggregations + end + name = part_id.id2name class_name = options[:class_name] || name.camelize mapping = options[:mapping] || [ name, name ] diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 2b0b2864bc..403667fb70 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -2,8 +2,8 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, predicate_builder, association) - super(klass, table, predicate_builder) + def initialize(klass, association) + super(klass) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 661605d3e5..1ee52945ea 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,7 +241,7 @@ module ActiveRecord association end - def association_cached?(name) # :nodoc + def association_cached?(name) # :nodoc: @association_cache.key?(name) end @@ -292,13 +292,13 @@ module ActiveRecord # # The project class now has the following methods (and more) to ease the traversal and # manipulation of its relationships: - # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> - # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> - # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> - # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> + # * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt> + # * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt> + # * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>, + # <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>, + # <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt> + # * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>, + # <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt> # # === A word of warning # @@ -1061,12 +1061,6 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * inverse associations for #belongs_to associations #has_many are ignored. - # # For more information, see the documentation for the +:inverse_of+ option. # # == Deleting from associations @@ -1238,9 +1232,9 @@ module ActiveRecord # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>) # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) - # * <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>) + # * <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>) # * <tt>Firm#clients.reload</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # @@ -1279,6 +1273,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many # association will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1352,8 +1349,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_many association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_many association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:extend] # Specifies a module or array of modules that will be extended into the association object returned. @@ -1409,9 +1405,9 @@ module ActiveRecord # An Account class declares <tt>has_one :beneficiary</tt>, which will add: # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>) # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) - # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) - # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) - # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) + # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new(account_id: id)</tt>) + # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save; b</tt>) + # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save!; b</tt>) # * <tt>Account#reload_beneficiary</tt> # # === Scopes @@ -1449,6 +1445,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association # will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1464,6 +1463,9 @@ module ActiveRecord # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. You can only use a <tt>:through</tt> query through a #has_one # or #belongs_to association on the join model. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:source] # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. @@ -1484,8 +1486,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_one association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_one association. # 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. @@ -1570,6 +1571,9 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key # of "favorite_person_id". + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the association with a "_type" @@ -1619,8 +1623,7 @@ module ActiveRecord # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] # Specifies the name of the #has_one or #has_many association on the associated - # object that is the inverse of this #belongs_to association. Does not work in - # combination with the <tt>:polymorphic</tt> options. + # object that is the inverse of this #belongs_to association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] # When set to +true+, the association will not have its presence validated. @@ -1743,8 +1746,8 @@ module ActiveRecord # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> # * <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>) + # * <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>) # * <tt>Developer#projects.reload</tt> # The declaration may include an +options+ hash to specialize the behavior of the association. # @@ -1789,6 +1792,9 @@ module ActiveRecord # of this class in lower-case and "_id" suffixed. So a Person class that makes # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 14881cfe17..272eede824 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -30,20 +30,12 @@ module ActiveRecord join.left.scan( /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size - elsif join.respond_to? :left + elsif join.is_a?(Arel::Nodes::Join) join.left.name == name ? 1 : 0 elsif join.is_a?(Hash) - join.fetch(name, 0) + join[name] 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 + raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join" end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ca1f9f1650..44596f4424 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -19,7 +19,6 @@ module ActiveRecord # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection - attr_accessor :inversed delegate :options, to: :reflection @@ -67,7 +66,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - !inversed && loaded? && @stale_state != stale_state + !@inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -98,23 +97,24 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = owner - inverse.inversed = true + if inverse = inverse_association_for(record) + inverse.inversed_from(owner) end record end # Remove the inverse association, if possible def remove_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = nil - inverse.inversed = false + if inverse = inverse_association_for(record) + inverse.inversed_from(nil) end end + def inversed_from(record) + self.target = record + @inversed = !!record + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -124,7 +124,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) + AssociationRelation.create(klass, self).merge!(klass.all) end def extensions @@ -156,9 +156,9 @@ module ActiveRecord reset end - # We can't dump @reflection since it contains the scope proc + # We can't dump @reflection and @through_reflection since it contains the scope proc def marshal_dump - ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } + ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] } [@reflection.name, ivars] end @@ -201,8 +201,8 @@ module ActiveRecord if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] - if reflection.options[:as] - attributes[reflection.type] = owner.class.base_class.name + if reflection.type + attributes[reflection.type] = owner.class.polymorphic_name end end @@ -240,6 +240,12 @@ module ActiveRecord end end + def inverse_association_for(record) + if invertible_for?(record) + record.association(inverse_reflection_for(record).name) + end + end + # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. @@ -269,6 +275,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| initialize_attributes(record, attributes) + yield(record) if block_given? end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 11967e0571..0a90a6104a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -35,24 +35,20 @@ module ActiveRecord binds << last_reflection.join_id_for(owner) if last_reflection.type - binds << owner.class.base_class.name + binds << owner.class.polymorphic_name end chain.each_cons(2).each do |reflection, next_reflection| if reflection.type - binds << next_reflection.klass.base_class.name + binds << next_reflection.klass.polymorphic_name end end binds end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value_transformation - private def join(table, constraint) table.create_join(table, table.create_on(constraint)) end @@ -67,7 +63,7 @@ module ActiveRecord scope = apply_scope(scope, table, key, value) if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) + polymorphic_type = transform_value(owner.class.polymorphic_name) scope = apply_scope(scope, table, reflection.type, polymorphic_type) end @@ -88,7 +84,7 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = transform_value(next_reflection.klass.base_class.name) + value = transform_value(next_reflection.klass.polymorphic_name) scope = apply_scope(scope, table, reflection.type, value) end @@ -135,7 +131,7 @@ module ActiveRecord item = eval_scope(reflection, scope_chain_item, owner) if scope_chain_item == chain_head.scope - scope.merge! item.except(:where, :includes) + scope.merge! item.except(:where, :includes, :unscope, :order) end reflection.all_includes do @@ -144,7 +140,7 @@ module ActiveRecord scope.unscope!(*item.unscope_values) scope.where_clause += item.where_clause - scope.order_values |= item.order_values + scope.order_values = item.order_values | scope.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 bd2012df84..3d4ad1dd5b 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -5,23 +5,18 @@ module ActiveRecord # = Active Record Belongs To Association class BelongsToAssociation < SingularAssociation #:nodoc: def handle_dependency - target.send(options[:dependent]) if load_target - end + return unless load_target - def replace(record) - if record - raise_on_type_mismatch!(record) - update_counters_on_replace(record) - set_inverse_instance(record) - @updated = true + case options[:dependent] + when :destroy + target.destroy + raise ActiveRecord::Rollback unless target.destroyed? else - decrement_counters + target.send(options[:dependent]) end - - self.target = record end - def target=(record) + def inversed_from(record) replace_keys(record) super end @@ -47,14 +42,32 @@ module ActiveRecord update_counters(1) end + def target_changed? + owner.saved_change_to_attribute?(reflection.foreign_key) + end + private + def replace(record) + if record + raise_on_type_mismatch!(record) + update_counters_on_replace(record) + set_inverse_instance(record) + @updated = true + else + decrement_counters + end + + replace_keys(record) + + self.target = record + end def update_counters(by) if require_counter_update? && foreign_key_present? if target && !stale_target? target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch]) + counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) end end end @@ -70,19 +83,22 @@ module ActiveRecord def update_counters_on_replace(record) if require_counter_update? && different_target?(record) owner.instance_variable_set :@_after_replace_counter_called, true - record.increment!(reflection.counter_cache_column) + record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) decrement_counters end end # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner._read_attribute(reflection.foreign_key) + record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record ? - record._read_attribute(reflection.association_primary_key(record.class)) : nil + owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil + end + + def primary_key(record) + reflection.association_primary_key(record.class) end def foreign_key_present? @@ -96,12 +112,9 @@ module ActiveRecord inverse && inverse.has_one? end - def target_id - if options[:primary_key] - owner.send(reflection.name).try(:id) - else - owner._read_attribute(reflection.foreign_key) - end + def counter_cache_target + primary_key = reflection.association_primary_key(klass) + klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key)) end def stale_state diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 55d789c66a..3fd2fb5f67 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -9,11 +9,14 @@ module ActiveRecord type.presence && type.constantize end - private + def target_changed? + super || owner.saved_change_to_attribute?(reflection.foreign_type) + end + private def replace_keys(record) super - owner[reflection.foreign_type] = record ? record.class.base_class.name : nil + owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index c161454c1a..b5fb0092d7 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -36,7 +36,7 @@ module ActiveRecord::Associations::Builder # :nodoc: if (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false - elsif saved_change_to_attribute?(foreign_key) && !new_record? + elsif association(reflection.name).target_changed? if reflection.polymorphic? model = attribute_in_database(reflection.foreign_type).try(:constantize) model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) @@ -48,15 +48,21 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key_was = attribute_before_last_save foreign_key foreign_key = attribute_in_database foreign_key - if foreign_key && model.respond_to?(:increment_counter) - model.increment_counter(cache_column, foreign_key) + if foreign_key && model < ActiveRecord::Base + counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1) end - if foreign_key_was && model_was.respond_to?(:decrement_counter) - model_was.decrement_counter(cache_column, foreign_key_was) + if foreign_key_was && model_was < ActiveRecord::Base + counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1) end end end + + private + def counter_cache_target(reflection, model, foreign_key) + primary_key = reflection.association_primary_key(model) + model.unscoped.where!(primary_key => foreign_key) + end end end @@ -84,7 +90,8 @@ module ActiveRecord::Associations::Builder # :nodoc: else klass = association.klass end - old_record = klass.find_by(klass.primary_key => old_foreign_id) + primary_key = reflection.association_primary_key(klass) + old_record = klass.find_by(primary_key => old_foreign_id) if old_record if touch != true 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 1981da11a2..e3070e0472 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 @@ -2,39 +2,6 @@ module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver # :nodoc: - KnownTable = Struct.new :join_table - - class KnownClass # :nodoc: - def initialize(lhs_class, rhs_class_name) - @lhs_class = lhs_class - @rhs_class_name = rhs_class_name - @join_table = nil - end - - def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") - end - - private - - def klass - @lhs_class.send(:compute_type, @rhs_class_name) - end - end - - def self.build(lhs_class, name, options) - if options[:join_table] - KnownTable.new options[:join_table].to_s - else - class_name = options.fetch(:class_name) { - name.to_s.camelize.singularize - } - KnownClass.new lhs_class, class_name.to_s - end - end - end - attr_reader :lhs_model, :association_name, :options def initialize(association_name, lhs_model, options) @@ -44,8 +11,6 @@ module ActiveRecord::Associations::Builder # :nodoc: end def through_model - habtm = JoinTableResolver.build lhs_model, association_name, options - join_model = Class.new(ActiveRecord::Base) { class << self attr_accessor :left_model @@ -56,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.table_name - table_name_resolver.join_table + # Table name needs to be resolved lazily + # because RHS class might not have been loaded + @table_name ||= table_name_resolver.call end def self.compute_type(class_name) @@ -86,7 +53,7 @@ module ActiveRecord::Associations::Builder # :nodoc: } join_model.name = "HABTM_#{association_name.to_s.camelize}" - join_model.table_name_resolver = habtm + join_model.table_name_resolver = -> { table_name } join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model @@ -117,6 +84,18 @@ module ActiveRecord::Associations::Builder # :nodoc: middle_options end + def table_name + if options[:join_table] + options[:join_table].to_s + else + class_name = options.fetch(:class_name) { + association_name.to_s.camelize.singularize + } + klass = lhs_model.send(:compute_type, class_name.to_s) + [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end + end + def belongs_to_options(options) rhs_options = {} diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index de8afc9ccb..840d900bbc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -45,6 +45,8 @@ module ActiveRecord def ids_reader if loaded? target.pluck(reflection.association_primary_key) + elsif !target.empty? + load_target.pluck(reflection.association_primary_key) else @association_ids ||= scope.pluck(reflection.association_primary_key) end @@ -53,7 +55,7 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) primary_key = reflection.association_primary_key - pk_type = klass.type_for_attribute(primary_key.to_s) + pk_type = klass.type_for_attribute(primary_key) ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } @@ -103,9 +105,7 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - end + add_to_target(build_record(attributes, &block)) end end @@ -212,9 +212,11 @@ module ActiveRecord def size if !find_target? || loaded? target.size + elsif @association_ids + @association_ids.size elsif !association_scope.group_values.empty? load_target.size - elsif !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && !target.empty? unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -231,10 +233,10 @@ module ActiveRecord # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - if loaded? + if loaded? || @association_ids size.zero? else - @target.blank? && !scope.exists? + target.empty? && !scope.exists? end end @@ -356,15 +358,18 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| _create_record(attr, raise, &block) } else + record = build_record(attributes, &block) transaction do - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - insert_record(record, true, raise) { + result = nil + add_to_target(record) do + result = insert_record(record, true, raise) { @_was_loaded = loaded? @association_ids = nil } end + raise ActiveRecord::Rollback unless result end + record end end @@ -395,7 +400,7 @@ module ActiveRecord records.each { |record| callback(:before_remove, record) } delete_records(existing_records, method) if existing_records.any? - records.each { |record| target.delete(record) } + @target -= records records.each { |record| callback(:after_remove, record) } end @@ -442,7 +447,9 @@ module ActiveRecord end end - result && records + raise ActiveRecord::Rollback unless result + + records end def replace_on_target(record, index, skip_callbacks) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8b4a48a38c..9a30198b95 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -32,7 +32,7 @@ module ActiveRecord class CollectionProxy < Relation def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table, klass.predicate_builder + super klass extensions = association.extensions extend(*extensions) if extensions.any? 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 adbf52b87c..617956c768 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,9 +8,7 @@ module ActiveRecord def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -50,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -97,7 +90,7 @@ module ActiveRecord def build_record(attributes) ensure_not_nested - record = super(attributes) + record = super inverse = source_reflection.inverse_of if inverse @@ -140,21 +133,15 @@ module ActiveRecord scope = through_association.scope scope.where! construct_join_attributes(*records) + scope = scope.where(through_scope_attributes) case method when :destroy if scope.klass.primary_key - count = scope.destroy_all.length + count = scope.destroy_all.count(&:destroyed?) else scope.each(&:_run_destroy_callbacks) - - arel = scope.arel - - stmt = Arel::DeleteManager.new - stmt.from scope.klass.arel_table - stmt.wheres = arel.constraints - - count = scope.klass.connection.delete(stmt, "SQL") + count = scope.delete_all end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7953b89f61..390bfd8b08 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -23,35 +23,6 @@ module ActiveRecord end end - def replace(record, save = true) - raise_on_type_mismatch!(record) if record - load_target - - return target unless target || record - - assigning_another_record = target != record - if assigning_another_record || record.has_changes_to_save? - save &&= owner.persisted? - - transaction_if(save) do - remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record - - if record - set_owner_attributes(record) - set_inverse_instance(record) - - if save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." - end - end - end - end - - self.target = record - end - def delete(method = options[:dependent]) if load_target case method @@ -60,6 +31,7 @@ module ActiveRecord when :destroy target.destroyed_by_association = reflection target.destroy + throw(:abort) unless target.destroyed? when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end @@ -67,6 +39,33 @@ module ActiveRecord end private + def replace(record, save = true) + raise_on_type_mismatch!(record) if record + + return target unless load_target || record + + assigning_another_record = target != record + if assigning_another_record || record.has_changes_to_save? + save &&= owner.persisted? + + transaction_if(save) do + remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record + + if record + set_owner_attributes(record) + set_inverse_instance(record) + + if save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end + end + end + end + + self.target = record + end # The reason that the save param for replace is false, if for create (not just build), # is because the setting of the foreign keys is actually handled by the scoping when @@ -106,6 +105,14 @@ module ActiveRecord yield end end + + def _create_record(attributes, raise_error = false, &block) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + + super + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 36746f9115..10978b2d93 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -6,17 +6,16 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation - def replace(record) - create_through_record(record) - self.target = record - end - private + def replace(record, save = true) + create_through_record(record, save) + self.target = record + end - def create_through_record(record) + def create_through_record(record, save) ensure_not_nested - through_proxy = owner.association(through_reflection.name) + through_proxy = through_association through_record = through_proxy.load_target if through_record && !record @@ -29,8 +28,12 @@ module ActiveRecord end if through_record - through_record.update(attributes) - elsif owner.new_record? + if through_record.new_record? + through_record.assign_attributes(attributes) + else + through_record.update(attributes) + end + elsif owner.new_record? || !save through_proxy.build(attributes) else through_proxy.create(attributes) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index df4bf07999..b76005b587 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -14,10 +14,8 @@ module ActiveRecord i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table, h| - h[table.node] = table.columns.map { |column| - [column.name, column.alias] - } + @columns_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns } end @@ -25,9 +23,8 @@ module ActiveRecord @tables.flat_map(&:column_aliases) end - # An array of [column_name, alias] pairs for the table def column_aliases(node) - @name_and_alias_cache[node] + @columns_cache[node] end def column_alias(node, column) @@ -67,64 +64,31 @@ module ActiveRecord end end - # base is the base class on which operation is taking place. - # associations is the list of associations which are joined using hash, symbol or array. - # joins is the list of all string join commands and arel nodes. - # - # Example : - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # has_many :patients, through: :appointments - # end - # - # If I execute `@physician.patients.to_a` then - # base # => Physician - # associations # => [] - # joins # => [#<Arel::Nodes::InnerJoin: ...] - # - # However if I execute `Physician.joins(:appointments).to_a` then - # base # => Physician - # associations # => [:appointments] - # joins # => [] - # - def initialize(base, table, associations, alias_tracker, eager_loading: true) - @alias_tracker = alias_tracker - @eager_loading = eager_loading + def initialize(base, table, associations) tree = self.class.make_tree associations @join_root = JoinBase.new(base, table, build(tree, base)) - @join_root.children.each { |child| construct_tables! @join_root, child } end def reflections join_root.drop(1).map!(&:reflection) end - def join_constraints(joins_to_add, join_type) - joins = join_root.children.flat_map { |child| - make_join_constraints(join_root, child, join_type) - } + def join_constraints(joins_to_add, join_type, alias_tracker) + @alias_tracker = alias_tracker + + construct_tables!(join_root) + joins = make_join_constraints(join_root, join_type) joins.concat joins_to_add.flat_map { |oj| + construct_tables!(oj.join_root) if join_root.match? oj.join_root walk join_root, oj.join_root else - oj.join_root.children.flat_map { |child| - make_join_constraints(oj.join_root, child, join_type) - } + make_join_constraints(oj.join_root, join_type) end } end - def aliases - @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| - columns = join_part.column_names.each_with_index.map { |column_name, j| - Aliases::Column.new column_name, "t#{i}_r#{j}" - } - Aliases::Table.new(join_part, columns) - } - end - def instantiate(result_set, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) @@ -149,35 +113,49 @@ module ActiveRecord result_set.each { |row_hash| parent_key = primary_key ? row_hash[primary_key] : row_hash parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + construct(parent, join_root, row_hash, seen, model_cache) } end parents.values end + def apply_column_aliases(relation) + relation._select!(-> { aliases.columns }) + end + protected - attr_reader :alias_tracker, :base_klass, :join_root + attr_reader :join_root private + attr_reader :alias_tracker - def make_constraints(parent, child, tables, join_type) - chain = child.reflection.chain - foreign_table = parent.table - foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain) + def aliases + @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part, columns) + } end - def make_outer_joins(parent, child) - join_type = Arel::Nodes::OuterJoin - make_join_constraints(parent, child, join_type, true) + def construct_tables!(join_root) + join_root.each_children do |parent, child| + child.tables = table_aliases_for(parent, child) + end end - def make_join_constraints(parent, child, join_type, aliasing = false) - tables = aliasing ? table_aliases_for(parent, child) : child.tables - joins = make_constraints(parent, child, tables, join_type) + def make_join_constraints(join_root, join_type) + join_root.children.flat_map do |child| + make_constraints(join_root, child, join_type) + end + end - joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } + def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin) + foreign_table = parent.table + foreign_klass = parent.base_klass + joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) } end def table_aliases_for(parent, node) @@ -190,13 +168,8 @@ module ActiveRecord } end - def construct_tables!(parent, node) - node.tables = table_aliases_for(parent, node) - node.children.each { |child| construct_tables! node, child } - end - def table_alias_for(reflection, parent, join) - name = "#{reflection.plural_name}_#{parent.table_name}" + name = reflection.alias_candidate(parent.table_name) join ? "#{name}_join" : name end @@ -205,8 +178,8 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_, n| make_outer_joins left, n } - intersection.flat_map { |l, r| walk l, r }.concat ojs + joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) } + joins.concat missing.flat_map { |_, n| make_constraints(left, n) } end def find_reflection(klass, name) @@ -221,15 +194,14 @@ module ActiveRecord reflection.check_eager_loadable! if reflection.polymorphic? - next unless @eager_loading raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) - end.compact + JoinAssociation.new(reflection, build(right, reflection.klass)) + end end - def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + def construct(ar_parent, parent, row, seen, model_cache) return if ar_parent.nil? parent.children.each do |node| @@ -238,7 +210,7 @@ module ActiveRecord other.loaded! elsif ar_parent.association_cached?(node.reflection.name) model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) next end @@ -250,25 +222,20 @@ module ActiveRecord next end - model = seen[ar_parent.object_id][node.base_klass][id] + model = seen[ar_parent.object_id][node][id] if model - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) else - model = construct_model(ar_parent, node, row, model_cache, id, aliases) - - if node.reflection.scope && - node.reflection.scope_for(node.base_klass.unscoped).readonly_value - model.readonly! - end + model = construct_model(ar_parent, node, row, model_cache, id) - seen[ar_parent.object_id][node.base_klass][id] = model - construct(model, node, row, rs, seen, model_cache, aliases) + seen[ar_parent.object_id][node][id] = model + construct(model, node, row, seen, model_cache) end end end - def construct_model(record, node, row, model_cache, id, aliases) + def construct_model(record, node, row, model_cache, id) other = record.association(node.reflection.name) model = model_cache[node][id] ||= @@ -282,6 +249,7 @@ module ActiveRecord other.target = model end + model.readonly! if node.readonly? model end 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 221c791bf8..4583d89cba 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -6,17 +6,14 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - # The reflection of the association represented - attr_reader :reflection + attr_reader :reflection, :tables + attr_accessor :table - attr_accessor :tables - - def initialize(reflection, children, alias_tracker) + def initialize(reflection, children) super(reflection.klass, children) - @alias_tracker = alias_tracker - @reflection = reflection - @tables = nil + @reflection = reflection + @tables = nil end def match?(other) @@ -24,14 +21,13 @@ module ActiveRecord super && reflection == other.reflection end - def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) - joins = [] - tables = tables.reverse + def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins = [] # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse_each do |reflection| - table = tables.shift + reflection.chain.reverse_each.with_index(1) do |reflection, i| + table = tables[-i] klass = reflection.klass constraint = reflection.build_join_constraint(table, foreign_table) @@ -54,12 +50,16 @@ module ActiveRecord joins end - def table - tables.first + def tables=(tables) + @tables = tables + @table = tables.first end - protected - attr_reader :alias_tracker + def readonly? + return @readonly if defined?(@readonly) + + @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 2181f308bf..3ad72a3646 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -33,6 +33,13 @@ module ActiveRecord children.each { |child| child.each(&block) } end + def each_children(&block) + children.each do |child| + yield self, child + child.each_children(&block) + end + end + # An Arel::Table for the active_record def table raise NotImplementedError @@ -47,8 +54,8 @@ module ActiveRecord length = column_names_with_alias.length while index < length - column_name, alias_name = column_names_with_alias[index] - hash[column_name] = row[alias_name] + column = column_names_with_alias[index] + hash[column.name] = row[column.alias] index += 1 end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index e1087be9b3..5c2ac5b374 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -83,7 +83,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = records.compact + records = Array.wrap(records).compact if records.empty? [] @@ -98,26 +98,30 @@ module ActiveRecord private # Loads all the given data into +records+ for the +association+. - def preloaders_on(association, records, scope) + def preloaders_on(association, records, scope, polymorphic_parent = false) case association when Hash - preloaders_for_hash(association, records, scope) + preloaders_for_hash(association, records, scope, polymorphic_parent) when Symbol - preloaders_for_one(association, records, scope) + preloaders_for_one(association, records, scope, polymorphic_parent) when String - preloaders_for_one(association.to_sym, records, scope) + preloaders_for_one(association.to_sym, records, scope, polymorphic_parent) else raise ArgumentError, "#{association.inspect} was not recognized for preload" end end - def preloaders_for_hash(association, records, scope) + def preloaders_for_hash(association, records, scope, polymorphic_parent) association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope + loaders = preloaders_for_one parent, records, scope, polymorphic_parent recs = loaders.flat_map(&:preloaded_records).uniq + + reflection = records.first.class._reflect_on_association(parent) + polymorphic_parent = reflection && reflection.options[:polymorphic] + loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + preloaders_on assoc, recs, scope, polymorphic_parent } loaders } @@ -135,8 +139,8 @@ module ActiveRecord # Additionally, polymorphic belongs_to associations can have multiple associated # classes, depending on the polymorphic_type field. So we group by the classes as # well. - def preloaders_for_one(association, records, scope) - grouped_records(association, records).flat_map do |reflection, klasses| + def preloaders_for_one(association, records, scope, polymorphic_parent) + grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self @@ -145,10 +149,11 @@ module ActiveRecord end end - def grouped_records(association, records) + def grouped_records(association, records, polymorphic_parent) h = {} records.each do |record| next unless record + next if polymorphic_parent && !record.class._reflect_on_association(association) assoc = record.association(association) next unless assoc.klass klasses = h[assoc.reflection] ||= {} @@ -169,7 +174,7 @@ module ActiveRecord owners.flat_map { |owner| owner.association(reflection.name).target } end - protected + private attr_reader :owners, :reflection end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 735da152b7..d6f7359055 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -27,10 +27,9 @@ module ActiveRecord end end - protected + private attr_reader :owners, :reflection, :preload_scope, :model, :klass - private # The name of the key on the associated records def association_key_name reflection.join_primary_key(klass) @@ -43,11 +42,11 @@ module ActiveRecord def associate_records_to_owner(owner, records) association = owner.association(reflection.name) + association.loaded! if reflection.collection? - association.loaded! association.target.concat(records) else - association.target = records.first + association.target = records.first unless records.empty? end end @@ -82,11 +81,11 @@ module ActiveRecord end def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type + @klass.type_for_attribute(association_key_name).type end def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type + @model.type_for_attribute(owner_key_name).type end def load_records(&block) @@ -118,7 +117,7 @@ module ActiveRecord scope = klass.scope_for_association if reflection.type - scope.where!(reflection.type => model.base_class.sti_name) + scope.where!(reflection.type => model.polymorphic_name) end scope.merge!(reflection_scope) if reflection.scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 441bd715e4..cfab16a745 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -17,9 +17,8 @@ module ActiveRecord replace(record) end - def build(attributes = {}) - record = build_record(attributes) - yield(record) if block_given? + def build(attributes = {}, &block) + record = build_record(attributes, &block) set_new_record(record) record end @@ -62,13 +61,8 @@ module ActiveRecord replace(record) end - def _create_record(attributes, raise_error = false) - unless owner.persisted? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - - record = build_record(attributes) - yield(record) if block_given? + def _create_record(attributes, raise_error = false, &block) + record = build_record(attributes, &block) saved = record.save set_new_record(record) raise RecordInvalid.new(record) if !saved && raise_error diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index bce2a95ce1..15e6565e69 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,9 +4,24 @@ module ActiveRecord module Associations # = Active Record Through Association module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, to: :reflection + delegate :source_reflection, to: :reflection private + def through_reflection + @through_reflection ||= begin + refl = reflection.through_reflection + + while refl.through_reflection? + refl = refl.through_reflection + end + + refl + end + end + + def through_association + @through_association ||= owner.association(through_reflection.name) + end # We merge in these scopes for two reasons: # @@ -38,24 +53,22 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + association_primary_key = source_reflection.association_primary_key(reflection.klass) + + if association_primary_key == reflection.klass.primary_key && !options[:source_type] 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)) - } + source_reflection.foreign_key => records.map(&association_primary_key.to_sym) } end if options[:source_type] - join_attributes[source_reflection.foreign_type] = - records.map { |record| record.class.base_class.name } + join_attributes[source_reflection.foreign_type] = [ options[:source_type] ] end if records.count == 1 - Hash[join_attributes.map { |k, v| [k, v.first] }] + join_attributes.transform_values!(&:first) else join_attributes end @@ -101,7 +114,7 @@ module ActiveRecord attributes[inverse.foreign_key] = target.id end - super(attributes) + super end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 8b0d9aab01..b6f0e18764 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -4,14 +4,8 @@ require "active_model/forbidden_attributes_protection" module ActiveRecord module AttributeAssignment - extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. - def attributes=(attributes) - assign_attributes(attributes) - end - private def _assign_attributes(attributes) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 64f81ca582..e4b8b1a330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -26,7 +26,7 @@ module ActiveRecord def self.set_name_cache(name, value) const_name = "ATTR_#{name}" unless const_defined? const_name - const_set const_name, value.dup.freeze + const_set const_name, -value end end } @@ -59,7 +59,7 @@ module ActiveRecord # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated - superclass.define_attribute_methods unless self == base_class + superclass.define_attribute_methods unless base_class? super(attribute_names) @attribute_methods_generated = true end @@ -175,9 +175,20 @@ module ActiveRecord # Regexp whitelist. Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" + # "#{table_name}.#{column_name} #{direction} NULLS FIRST" + # "#{table_name}.#{column_name} NULLS LAST" # "#{column_name}" # "#{column_name} #{direction}" - COLUMN_NAME_ORDER_WHITELIST = /\A(?:\w+\.)?\w+(?:\s+asc|\s+desc)?\z/i + # "#{column_name} #{direction} NULLS FIRST" + # "#{column_name} NULLS LAST" + COLUMN_NAME_ORDER_WHITELIST = / + \A + (?:\w+\.)? + \w+ + (?:\s+asc|\s+desc)? + (?:\s+nulls\s+(?:first|last))? + \z + /ix def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: unexpected = args.reject do |arg| @@ -432,33 +443,24 @@ module ActiveRecord @attributes.accessed end - protected - - def attribute_method?(attr_name) # :nodoc: + private + def attribute_method?(attr_name) # We check defined? because Syck calls respond_to? before actually calling initialize. defined?(@attributes) && @attributes.key?(attr_name) end - private - - def arel_attributes_with_values_for_create(attribute_names) - arel_attributes_with_values(attributes_for_create(attribute_names)) + def attributes_with_values_for_create(attribute_names) + attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) - arel_attributes_with_values(attributes_for_update(attribute_names)) + def attributes_with_values_for_update(attribute_names) + attributes_with_values(attributes_for_update(attribute_names)) end - # Returns a Hash of the Arel::Attributes and attribute values that have been - # typecasted for use in an Arel insert/update method. - def arel_attributes_with_values(attribute_names) - attrs = {} - arel_table = self.class.arel_table - - attribute_names.each do |name| - attrs[arel_table[name]] = typecasted_attribute_value(name) + def attributes_with_values(attribute_names) + attribute_names.each_with_object({}) do |name, attrs| + attrs[name] = _read_attribute(name) end - attrs end # Filters the primary keys and readonly attributes from the attribute names. @@ -483,9 +485,5 @@ module ActiveRecord def pk_attribute?(name) name == self.class.primary_key end - - def typecasted_attribute_value(name) - _read_attribute(name) - end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..233ee29fac 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -39,11 +39,12 @@ module ActiveRecord end end - # Did this attribute change when we last saved? This method can be invoked - # as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>. - # Behaves similarly to +attribute_changed?+. This method is useful in - # after callbacks to determine if the call to save changed a certain - # attribute. + # Did this attribute change when we last saved? + # + # This method is useful in after callbacks to determine if an attribute + # was changed during the save that triggered the callbacks to run. It can + # be invoked as +saved_change_to_name?+ instead of + # <tt>saved_change_to_attribute?("name")</tt>. # # ==== Options # @@ -60,19 +61,20 @@ module ActiveRecord # attribute was changed, the result will be an array containing the # original value and the saved value. # - # Behaves similarly to +attribute_change+. This method is useful in after - # callbacks, to see the change in an attribute that just occurred - # - # This method can be invoked as +saved_change_to_name+ in instead of - # <tt>saved_change_to_attribute("name")</tt> + # This method is useful in after callbacks, to see the change in an + # attribute during the save that triggered the callbacks to run. It can be + # invoked as +saved_change_to_name+ instead of + # <tt>saved_change_to_attribute("name")</tt>. def saved_change_to_attribute(attr_name) mutations_before_last_save.change_to_attribute(attr_name) end # Returns the original value of an attribute before the last save. - # Behaves similarly to +attribute_was+. This method is useful in after - # callbacks to get the original value of an attribute before the save that - # just occurred + # + # This method is useful in after callbacks to get the original value of an + # attribute before the save that triggered the callbacks to run. It can be + # invoked as +name_before_last_save+ instead of + # <tt>attribute_before_last_save("name")</tt>. def attribute_before_last_save(attr_name) mutations_before_last_save.original_value(attr_name) end @@ -87,39 +89,75 @@ module ActiveRecord mutations_before_last_save.changes end - # Alias for +attribute_changed?+ + # Will this attribute change the next time we save? + # + # This method is useful in validations and before callbacks to determine + # if the next call to +save+ will change a particular attribute. It can be + # invoked as +will_save_change_to_name?+ instead of + # <tt>will_save_change_to_attribute("name")</tt>. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value will be + # changed to the given value def will_save_change_to_attribute?(attr_name, **options) mutations_from_database.changed?(attr_name, **options) end - # Alias for +attribute_change+ + # Returns the change to an attribute that will be persisted during the + # next save. + # + # This method is useful in validations and before callbacks, to see the + # change to an attribute that will occur when the record is saved. It can + # be invoked as +name_change_to_be_saved+ instead of + # <tt>attribute_change_to_be_saved("name")</tt>. + # + # If the attribute will change, the result will be an array containing the + # original value and the new value about to be saved. def attribute_change_to_be_saved(attr_name) mutations_from_database.change_to_attribute(attr_name) end - # Alias for +attribute_was+ + # Returns the value of an attribute in the database, as opposed to the + # in-memory value that will be persisted the next time the record is + # saved. + # + # This method is useful in validations and before callbacks, to see the + # original value of an attribute prior to any changes about to be + # saved. It can be invoked as +name_in_database+ instead of + # <tt>attribute_in_database("name")</tt>. def attribute_in_database(attr_name) mutations_from_database.original_value(attr_name) end - # Alias for +changed?+ + # Will the next call to +save+ have any changes to persist? def has_changes_to_save? mutations_from_database.any_changes? end - # Alias for +changes+ + # Returns a hash containing all the changes that will be persisted during + # the next save. def changes_to_save mutations_from_database.changes end - # Alias for +changed+ + # Returns an array of the names of any attributes that will change when + # the record is next saved. def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end - # Alias for +changed_attributes+ + # Returns a hash of the attributes that will change when the record is + # next saved. + # + # The hash keys are the attribute names, and the hash values are the + # original attribute values in the database (as opposed to the in-memory + # values about to be saved). def attributes_in_database - changes_to_save.transform_values(&:first) + mutations_from_database.changed_values end private diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index d8fc046e10..9b267bb7c0 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -83,7 +83,7 @@ module ActiveRecord end def reset_primary_key #:nodoc: - if self == base_class + if base_class? self.primary_key = get_primary_key(base_class.name) else self.primary_key = base_class.primary_key @@ -131,7 +131,7 @@ module ActiveRecord def suppress_composite_primary_key(pk) return pk unless pk.is_a?(Array) - warn <<-WARNING.strip_heredoc + warn <<~WARNING WARNING: Active Record does not support composite primary key. #{table_name} has composite primary key. Composite primary key is ignored. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4077250583..0f7bcba564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -27,7 +27,7 @@ module ActiveRecord # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @@ -69,7 +69,7 @@ module ActiveRecord if defined?(JRUBY_VERSION) # This form is significantly faster on JRuby, and this is one of our biggest hotspots. # https://github.com/jruby/jruby/pull/2562 - def _read_attribute(attr_name, &block) # :nodoc + def _read_attribute(attr_name, &block) # :nodoc: @attributes.fetch_value(attr_name.to_s, &block) end else diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index ebc2baed34..6e0e90f39c 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -7,7 +7,7 @@ module ActiveRecord class ColumnNotSerializableError < StandardError def initialize(name, type) - super <<-EOS.strip_heredoc + super <<~EOS Column `#{name}` of type #{type.class} does not support `serialize` feature. Usually it means that you are trying to use `serialize` on a column that already implements serialization natively. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bb0ec6a8c3..c7521422bb 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,7 +13,7 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a1250c3835..a405f05e0b 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each do |record| + records.each_with_index do |record, index| next if record.destroyed? saved = true @@ -400,8 +400,13 @@ module ActiveRecord if autosave != false && (@new_record_before_save || record.new_record?) if autosave saved = association.insert_record(record, false) - else - association.insert_record(record) unless reflection.nested? + elsif !reflection.nested? + if reflection.validate? + valid = association_valid?(reflection, record, index) + saved = valid ? association.insert_record(record, false) : false + else + association.insert_record(record) + end end elsif autosave saved = record.save(validate: false) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index b7ad944cec..5169f312f5 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,7 +9,6 @@ require "active_support/core_ext/module/attribute_accessors" 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" @@ -289,8 +288,10 @@ module ActiveRecord #:nodoc: extend Enum extend Delegation::DelegateCache extend CollectionCacheKey + extend Aggregations::ClassMethods include Core + include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema @@ -314,7 +315,6 @@ module ActiveRecord #:nodoc: include ActiveModel::SecurePassword include AutosaveAssociation include NestedAttributes - include Aggregations include Transactions include TouchLater include NoTouching diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 9dbdf845bd..b6852bfc71 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -75,21 +75,7 @@ module ActiveRecord # end # # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is - # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation - # where the +before_destroy+ method is overridden: - # - # class Topic < ActiveRecord::Base - # def before_destroy() destroy_author end - # end - # - # class Reply < Topic - # def before_destroy() destroy_readers end - # end - # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. - # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant - # to decide whether they want to call +super+ and trigger the inherited callbacks. + # run, both +destroy_author+ and +destroy_readers+ are called. # # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the # callbacks before specifying the associations. Otherwise, you might trigger the loading of a @@ -142,7 +128,7 @@ module ActiveRecord # end # end # - # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has + # So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other # initialization data such as the name of the attribute to work with: # @@ -332,6 +318,10 @@ module ActiveRecord _run_touch_callbacks { super } end + def increment!(*, touch: nil) # :nodoc: + touch ? _run_touch_callbacks { super } : super + end + private def create_or_update(*) diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 0520591f4f..dfba78614e 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -15,7 +15,7 @@ module ActiveRecord if collection.eager_loading? collection = collection.send(:apply_join_dependency) end - column_type = type_for_attribute(timestamp_column.to_s) + column_type = type_for_attribute(timestamp_column) column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column)) select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" 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 c730584902..f721e91203 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1011,8 +1011,8 @@ module ActiveRecord # Returns true if a connection that's accessible to this class has # already been opened. def connected?(spec_name) - conn = retrieve_connection_pool(spec_name) - conn && conn.connected? + pool = retrieve_connection_pool(spec_name) + pool && pool.connected? end # Remove the connection for this class. This will close the active 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 36048bee03..41553cfa83 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -356,38 +356,36 @@ module ActiveRecord # Inserts a set of fixtures into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG return if fixtures.empty? - columns = schema_cache.columns_hash(table_name) + execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") + end - values = fixtures.map do |fixture| - fixture = fixture.stringify_keys + def insert_fixtures_set(fixture_set, tables_to_delete = []) + fixture_inserts = fixture_set.map do |table_name, fixtures| + next if fixtures.empty? - unknown_columns = fixture.keys - columns.keys - if unknown_columns.any? - raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) - end + build_fixture_sql(fixtures, table_name) + end.compact - columns.map do |name, column| - if fixture.key?(name) - type = lookup_cast_type_from_column(column) - bind = Relation::QueryAttribute.new(name, fixture[name], type) - with_yaml_fallback(bind.value_for_database) - else - Arel.sql("DEFAULT") + table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) + + disable_referential_integrity do + transaction(requires_new: true) do + total_sql.each do |sql| + execute sql, "Fixtures Load" + yield if block_given? end end end - - table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) - columns.each_key { |column| manager.columns << table[column] } - manager.values = manager.create_values_list(values) - execute manager.to_sql, "Fixtures Insert" end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "DEFAULT VALUES" end @@ -416,6 +414,44 @@ module ActiveRecord alias join_to_delete join_to_update private + def default_insert_value(column) + Arel.sql("DEFAULT") + end + + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + default_insert_value(column) + end + end + end + + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end # Returns a subquery for the given key using the join information. def subquery_for(key, select) 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 25622e34c8..8aeb934ec2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -110,12 +110,7 @@ module ActiveRecord if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument( "sql.active_record", - sql: sql, - binds: binds, - type_casted_binds: -> { type_casted_binds(binds) }, - name: name, - connection_id: object_id, - cached: true, + cache_notification_info(sql, name, binds) ) @query_cache[sql][binds] else @@ -125,6 +120,19 @@ module ActiveRecord end end + # Database adapters can override this method to + # provide custom cache information. + def cache_notification_info(sql, name, binds) + { + sql: sql, + binds: binds, + type_casted_binds: -> { type_casted_binds(binds) }, + name: name, + connection_id: object_id, + cached: true + } + end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such # queries should not be cached. def locked?(arel) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 92e46ccf9f..98b1348135 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -130,7 +130,8 @@ module ActiveRecord end def quoted_time(value) # :nodoc: - quoted_date(value).sub(/\A2000-01-01 /, "") + value = value.change(year: 2000, month: 1, day: 1) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "") end def quoted_binary(value) # :nodoc: 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 4a191d337c..529c9d8ca6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractAdapter @@ -17,9 +15,8 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, + to: :@conn, private: true private @@ -66,7 +63,7 @@ module ActiveRecord end def visit_ForeignKeyDefinition(o) - sql = <<-SQL.strip_heredoc + sql = +<<~SQL 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)}) @@ -133,7 +130,7 @@ module ActiveRecord when :cascade then "ON #{action} CASCADE" when :restrict then "ON #{action} RESTRICT" else - raise ArgumentError, <<-MSG.strip_heredoc + raise ArgumentError, <<~MSG '#{dependency}' is not supported for :on_update or :on_delete. Supported values are: :nullify, :cascade, :restrict MSG 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 0594b4b485..582ac516c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -101,6 +101,10 @@ module ActiveRecord end alias validated? validate? + def export_name_on_schema_dump? + name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + end + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s @@ -151,13 +155,8 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options - private + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options def as_options(value) value.is_a?(Hash) ? value : {} @@ -357,8 +356,12 @@ module ActiveRecord type = type.to_sym if type options = options.dup - if @columns_hash[name] && @columns_hash[name].primary_key? - raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + if @columns_hash[name] + if @columns_hash[name].primary_key? + raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + else + raise ArgumentError, "you can't define an already defined column '#{name}'." + end end index_options = options.delete(:index) @@ -498,6 +501,9 @@ module ActiveRecord # t.date # t.binary # t.boolean + # t.foreign_key + # t.json + # t.virtual # t.remove # t.remove_references # t.remove_belongs_to @@ -662,19 +668,19 @@ module ActiveRecord # Adds a foreign key. # - # t.foreign_key(:authors) + # t.foreign_key(:authors) # # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] - def foreign_key(*args) # :nodoc: + def foreign_key(*args) @base.add_foreign_key(name, *args) end # Checks to see if a foreign key exists. # - # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) + # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] - def foreign_key_exists?(*args) # :nodoc: + def foreign_key_exists?(*args) @base.foreign_key_exists?(name, *args) end end 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 1926603474..622e00fffb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/compact" - module ActiveRecord module ConnectionAdapters # :nodoc: class SchemaDumper < SchemaDumper # :nodoc: 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 4f58b0242c..3be0906f2a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -305,7 +305,7 @@ module ActiveRecord yield td if block_given? if options[:force] - drop_table(table_name, **options, if_exists: true) + drop_table(table_name, options.merge(if_exists: true)) end result = execute schema_creation.accept td @@ -406,7 +406,7 @@ module ActiveRecord # # Defaults to false. # - # Only supported on the MySQL adapter, ignored elsewhere. + # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere. # # ====== Add a column # @@ -715,7 +715,7 @@ module ActiveRecord # # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). + # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it). # # ====== Creating a partial index # @@ -741,22 +741,13 @@ module ActiveRecord # ====== Creating an index with a specific operator class # # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops) - # - # generates: - # - # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL + # # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL # # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops }) - # - # generates: - # - # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL + # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL # # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops) - # - # generates: - # - # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL + # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL # # Note: only supported by PostgreSQL # @@ -908,7 +899,7 @@ module ActiveRecord foreign_key_options = { to_table: reference_name } end foreign_key_options[:column] ||= "#{ref_name}_id" - remove_foreign_key(table_name, **foreign_key_options) + remove_foreign_key(table_name, foreign_key_options) end remove_column(table_name, "#{ref_name}_id") @@ -1049,8 +1040,8 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| - ActiveRecord::Migrator.parse_migration_filename(file).first.to_i + versions = migration_context.migration_files.map do |file| + migration_context.parse_migration_filename(file).first.to_i end unless migrated.include?(version) @@ -1062,13 +1053,7 @@ module ActiveRecord if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - if supports_multi_insert? - execute insert_versions_sql(inserting) - else - inserting.each do |v| - execute insert_versions_sql(v) - end - end + execute insert_versions_sql(inserting) 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 d9ac8db6a8..b59df2fff7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -17,11 +17,19 @@ module ActiveRecord end def committed? - @state == :committed + @state == :committed || @state == :fully_committed + end + + def fully_committed? + @state == :fully_committed end def rolledback? - @state == :rolledback + @state == :rolledback || @state == :fully_rolledback + end + + def fully_rolledback? + @state == :fully_rolledback end def fully_completed? @@ -55,10 +63,19 @@ module ActiveRecord @state = :rolledback end + def full_rollback! + @children.each { |c| c.rollback! } + @state = :fully_rolledback + end + def commit! @state = :committed end + def full_commit! + @state = :fully_committed + end + def nullify! @state = nil end @@ -75,7 +92,6 @@ module ActiveRecord class Transaction #:nodoc: attr_reader :connection, :state, :records, :savepoint_name - attr_writer :joinable def initialize(connection, options, run_commit_callbacks: false) @connection = connection @@ -89,10 +105,6 @@ module ActiveRecord records << record end - def rollback - @state.rollback! - end - def rollback_records ite = records.uniq while record = ite.shift @@ -104,10 +116,6 @@ module ActiveRecord end end - def commit - @state.commit! - end - def before_commit_records records.uniq.each(&:before_committed!) if @run_commit_callbacks end @@ -146,12 +154,12 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) - super + @state.rollback! end def commit connection.release_savepoint(savepoint_name) - super + @state.commit! end def full_rollback?; false; end @@ -169,12 +177,12 @@ module ActiveRecord def rollback connection.rollback_db_transaction - super + @state.full_rollback! end def commit connection.commit_db_transaction - super + @state.full_commit! 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 fc80d332f9..a4748dbeda 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -81,7 +81,9 @@ module ActiveRecord alias :in_use? :owner def self.type_cast_config_to_integer(config) - if config =~ SIMPLE_INT + if config.is_a?(Integer) + config + elsif SIMPLE_INT.match?(config) config.to_i else config @@ -119,6 +121,14 @@ module ActiveRecord end end + def migrations_paths # :nodoc: + @config[:migrations_paths] || Migrator.migrations_paths + end + + def migration_context # :nodoc: + MigrationContext.new(migrations_paths) + end + class Version include Comparable @@ -129,6 +139,10 @@ module ActiveRecord def <=>(version_string) @version <=> version_string.split(".").map(&:to_i) end + + def to_s + @version.join(".") + end end def valid_type?(type) # :nodoc: @@ -312,12 +326,18 @@ module ActiveRecord def supports_multi_insert? true end + deprecate :supports_multi_insert? # Does this adapter support virtual columns? def supports_virtual_columns? false end + # Does this adapter support foreign/external tables? + def supports_foreign_tables? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -537,12 +557,7 @@ module ActiveRecord end def extract_limit(sql_type) - case sql_type - when /^bigint/i - 8 - when /\((.*)\)/ - $1.to_i - end + $1.to_i if sql_type =~ /\((.*)\)/ end def translate_exception_class(e, sql) 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 0afdd959f5..9de8242a58 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -11,8 +11,6 @@ require "active_record/connection_adapters/mysql/schema_dumper" require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter @@ -46,7 +44,7 @@ module ActiveRecord class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) - stmt[:stmt].close + stmt.close end end @@ -225,7 +223,7 @@ module ActiveRecord end end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "VALUES ()" end @@ -249,7 +247,7 @@ module ActiveRecord # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" else execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end @@ -284,7 +282,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) - query_value(<<-SQL.strip_heredoc, "SCHEMA").presence + query_value(<<~SQL, "SCHEMA").presence SELECT table_comment FROM information_schema.tables WHERE table_schema = #{scope[:schema]} @@ -392,7 +390,7 @@ module ActiveRecord scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<~SQL, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', fk.referenced_column_name AS 'primary_key', fk.column_name AS 'column', @@ -480,7 +478,7 @@ module ActiveRecord scope = quoted_scope(table_name) - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' @@ -515,7 +513,7 @@ module ActiveRecord s.gsub(/\s+(?:ASC|DESC)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def strict_mode? @@ -526,23 +524,38 @@ module ActiveRecord index.using == :btree || super end - def insert_fixtures(*) - without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + def insert_fixtures_set(fixture_set, tables_to_delete = []) + with_multi_statements do + super { discard_remaining_results } + end end private + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + sql << ";\n" + if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty? + total_sql_chunks << sql + else + previous_packet << sql + end + end + end - def without_sql_mode(mode) - result = execute("SELECT @@SESSION.sql_mode") - current_mode = result.first[0] - return yield unless current_mode.include?(mode) + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + false + else + (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet + end + end - sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") - yield - ensure - sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") + def max_allowed_packet + bytes_margin = 2 + @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) end def initialize_type_map(m = type_map) @@ -606,6 +619,7 @@ module ActiveRecord ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 ER_DO_NOT_HAVE_DEFAULT = 1364 + ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 ER_DATA_TOO_LONG = 1406 ER_OUT_OF_RANGE = 1264 @@ -620,7 +634,7 @@ module ActiveRecord case error_number(exception) when ER_DUP_ENTRY RecordNotUnique.new(message) - when ER_NO_REFERENCED_ROW_2 + when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) when ER_CANNOT_ADD_FOREIGN mismatched_foreign_key(message) diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 508132accb..204691006c 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -57,9 +57,7 @@ module ActiveRecord private - def uri - @uri - end + attr_reader :uri def uri_parser @uri_parser ||= URI::Parser.new @@ -156,7 +154,6 @@ module ActiveRecord env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) end - config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } config.merge! env_config if env_config config.each do |key, value| diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index a058a72872..d89eeb7f54 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord else super end - @connection.next_result while @connection.more_results? + discard_remaining_results result end @@ -50,11 +50,54 @@ module ActiveRecord alias :exec_update :exec_delete private + def default_insert_value(column) + Arel.sql("DEFAULT") unless column.auto_increment? + end def last_inserted_id(result) @connection.last_id end + def discard_remaining_results + @connection.abandon_results! + end + + def supports_set_server_option? + @connection.respond_to?(:set_server_option) + end + + def multi_statements_enabled?(flags) + if flags.is_a?(Array) + flags.include?("MULTI_STATEMENTS") + else + (flags & Mysql2::Client::MULTI_STATEMENTS) != 0 + end + end + + def with_multi_statements + previous_flags = @config[:flags] + + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON) + else + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! + end + end + + yield + ensure + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF) + else + @config[:flags] = previous_flags + reconnect! + end + end + end + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection @@ -64,10 +107,7 @@ module ActiveRecord log(sql, name, binds, type_casted_binds) do if cache_stmt - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] + stmt = @statements[sql] ||= @connection.prepare(sql) else stmt = @connection.prepare(sql) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 75377693c6..c9ea653b77 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -4,8 +4,7 @@ module ActiveRecord module ConnectionAdapters module MySQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: - delegate :add_sql_comment!, :mariadb?, to: :@conn - private :add_sql_comment!, :mariadb? + delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true private diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index ce50590651..1cf210d85b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -36,7 +36,7 @@ module ActiveRecord end indexes.last[-2] << row[:Column_name] - indexes.last[-1][:lengths].merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] + indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" end end @@ -80,8 +80,8 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) - default, default_function = nil, "CURRENT_TIMESTAMP" + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default]) + default, default_function = nil, field[:Default] else default, default_function = field[:Default], nil end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index bfdc7995f0..544d720428 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,7 +3,7 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", "~> 0.4.4" +gem "mysql2", ">= 0.4.4", "< 0.6.0" require "mysql2" module ActiveRecord @@ -117,7 +117,7 @@ module ActiveRecord end def configure_connection - @connection.query_options.merge!(as: :array) + @connection.query_options[:as] = :array super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 469ef3f5a0..3ccc7271ab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -20,10 +20,9 @@ module ActiveRecord end end - protected + private attr_reader :max_identifier_length - private def sequence_name_from_parts(table_name, column_name, suffix) over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 542ca75d3e..247a25054e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -5,6 +5,7 @@ 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" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index d6852082ac..26abeea7ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -66,6 +66,10 @@ module ActiveRecord deserialize(raw_old_value) != new_value end + def force_equality?(value) + value.is_a?(::Array) + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 587e95d192..e9a79526f9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -43,10 +43,7 @@ module ActiveRecord /\A[0-9A-F]*\Z/i.match?(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value 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..24a1daa95a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date # :nodoc: + def cast_value(value) + 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 + 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 index 6edb7cfd3c..d85f9ab3ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -53,6 +53,10 @@ module ActiveRecord ::Range.new(new_begin, new_end, value.exclude_end?) end + def force_equality?(value) + value.is_a?(::Range) + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 6047217fcd..206b855a18 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -13,10 +13,10 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +gen_random_uuid()+ function from the + # By default, this will use the <tt>gen_random_uuid()</tt> function from the # +pgcrypto+ extension. As that extension is only available in # PostgreSQL 9.4+, for earlier versions an explicit default can be set - # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: "uuid_generate_v4()" 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 a8895f8606..26b8113b0d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -38,7 +38,7 @@ module ActiveRecord " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" - else + else "" end end @@ -107,7 +107,7 @@ module ActiveRecord oid = row[4] comment = row[5] - using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten orders = {} opclasses = {} @@ -115,7 +115,7 @@ module ActiveRecord if indkey.include?(0) columns = expressions else - columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact SELECT a.attnum, a.attname FROM pg_attribute a WHERE a.attrelid = #{oid} @@ -124,9 +124,13 @@ module ActiveRecord # add info on sort order (only desc order is explicitly specified, asc is the default) # and non-default opclasses - expressions.scan(/(\w+)(?: (?!DESC)(\w+))?(?: (DESC))?/).each do |column, opclass, desc| + expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| opclasses[column] = opclass.to_sym if opclass - orders[column] = :desc if desc + if nulls + orders[column] = [desc, nulls].compact.join(" ") + else + orders[column] = :desc if desc + end end end @@ -154,7 +158,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name, type: "BASE TABLE") if scope[:name] - query_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<~SQL, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -349,7 +353,7 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT a.attname FROM ( SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx @@ -375,7 +379,7 @@ module ActiveRecord if respond_to?(method, true) sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) } sql_fragments << sqls - non_combinable_operations << procs if procs.present? + non_combinable_operations.concat(procs) else execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? non_combinable_operations.each(&:call) @@ -498,7 +502,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<~SQL, "SCHEMA") SELECT t2.oid::regclass::text 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, c.convalidated AS valid FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid @@ -527,6 +531,14 @@ module ActiveRecord end end + def foreign_tables + query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA") + end + + def foreign_table_exists?(table_name) + query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present? + end + # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -571,7 +583,7 @@ module ActiveRecord .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def update_table_definition(table_name, base) # :nodoc: @@ -739,7 +751,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" @@ -753,9 +765,11 @@ module ActiveRecord type = \ case type when "BASE TABLE" - "'r'" + "'r','p'" when "VIEW" "'v','m'" + when "FOREIGN TABLE" + "'f'" end scope = {} scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index b252a76caa..ffd3be26b0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module ActiveRecord + # :stopdoc: module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) undef to_yaml if method_defined?(:to_yaml) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 441be47fa1..fdf6f75108 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility -gem "pg", "~> 0.18" +gem "pg", ">= 0.18", "< 2.0" require "pg" require "active_record/connection_adapters/abstract_adapter" @@ -281,7 +281,7 @@ module ActiveRecord end def discard! # :nodoc: - @connection.socket_io.reopen(IO::NULL) + @connection.socket_io.reopen(IO::NULL) rescue nil @connection = nil end @@ -318,6 +318,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_foreign_tables? + postgresql_version >= 90300 + end + def supports_pgcrypto_uuid? postgresql_version >= 90400 end @@ -461,7 +465,7 @@ module ActiveRecord 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", Type::Date.new + m.register_type "date", OID::Date.new m.register_type "money", OID::Money.new m.register_type "bytea", OID::Bytea.new @@ -833,6 +837,7 @@ module ActiveRecord ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql) ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql) ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index f34b6733da..c29cf1f9a1 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -28,7 +28,7 @@ module ActiveRecord coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources - coder["version"] = ActiveRecord::Migrator.current_version + coder["version"] = connection.migration_context.current_version end def init_with(coder) @@ -100,7 +100,7 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. - @version = ActiveRecord::Migrator.current_version + @version = connection.migration_context.current_version [@version, @columns, @columns_hash, @primary_keys, @data_sources] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 8042dbfea2..abedf01f10 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -17,7 +17,8 @@ module ActiveRecord end def quoted_time(value) - quoted_date(value) + value = value.change(year: 2000, month: 1, day: 1) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") end def quoted_binary(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 58e5138e02..24e7bc65fa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -7,6 +7,10 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name) exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + # Indexes SQLite creates implicitly for internal use start with "sqlite_". + # See https://www.sqlite.org/fileformat2.html#intschema + next if row["name"].starts_with?("sqlite_") + index_sql = query_value(<<-SQL, "SCHEMA") SELECT sql FROM sqlite_master @@ -40,7 +44,7 @@ module ActiveRecord where: where, orders: orders ) - end + end.compact end def create_schema_dumper(options) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d4f5bd16ac..bee74dc33d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -15,6 +15,8 @@ require "sqlite3" module ActiveRecord module ConnectionHandling # :nodoc: def sqlite3_connection(config) + config = config.symbolize_keys + # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -31,7 +33,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database].to_s, - results_as_hash: true + config.merge(results_as_hash: true) ) db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] @@ -94,16 +96,20 @@ module ActiveRecord class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) - stmt[:stmt].close unless stmt[:stmt].closed? + stmt.close unless stmt.closed? end end def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @active = nil + @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + if sqlite_version < "3.8.0" + raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." + end + configure_connection end @@ -116,7 +122,7 @@ module ActiveRecord end def supports_partial_index? - sqlite_version >= "3.8.0" + true end def requires_reloading? @@ -124,7 +130,7 @@ module ActiveRecord end def supports_foreign_keys_in_create? - sqlite_version >= "3.6.19" + true end def supports_views? @@ -139,12 +145,8 @@ module ActiveRecord true end - def supports_multi_insert? - sqlite_version >= "3.7.11" - end - def active? - @active != false + @active end # Disconnects from the database if already connected. Otherwise, this @@ -187,13 +189,16 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: - old = query_value("PRAGMA foreign_keys") + old_foreign_keys = query_value("PRAGMA foreign_keys") + old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys") begin + execute("PRAGMA defer_foreign_keys = ON") execute("PRAGMA foreign_keys = OFF") yield ensure - execute("PRAGMA foreign_keys = #{old}") + execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}") + execute("PRAGMA foreign_keys = #{old_foreign_keys}") end end @@ -224,11 +229,8 @@ module ActiveRecord stmt.close end else - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns + stmt = @statements[sql] ||= @connection.prepare(sql) + cols = stmt.columns stmt.reset! stmt.bind_params(type_casted_binds) records = stmt.to_a @@ -367,8 +369,22 @@ module ActiveRecord end def insert_fixtures(rows, table_name) - rows.each do |row| - insert_fixture(row, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG + insert_fixtures_set(table_name => rows) + end + + def insert_fixtures_set(fixture_set, tables_to_delete = []) + disable_referential_integrity do + transaction(requires_new: true) do + tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } + + fixture_set.each do |table_name, rows| + rows.each { |row| insert_fixture(row, table_name) } + end + end end end @@ -396,9 +412,11 @@ module ActiveRecord caller = lambda { |definition| yield definition if block_given? } transaction do - move_table(table_name, altered_table_name, - options.merge(temporary: true)) - move_table(altered_table_name, table_name, &caller) + disable_referential_integrity do + move_table(table_name, altered_table_name, + options.merge(temporary: true)) + move_table(altered_table_name, table_name, &caller) + end end end @@ -439,9 +457,6 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name - # indexes sqlite creates for internal use start with `sqlite_` and - # don't need to be copied - next if name.starts_with?("sqlite_") if to == "a#{from}" name = "t#{name}" elsif from == "a#{to}" diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 88d28dc52a..ee0e651912 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -57,6 +57,10 @@ module ActiveRecord spec = resolver.resolve(config).symbolize_keys spec[:name] = spec_name + # use the primary config if a config is not passed in and + # it's a three tier config + spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] + connection_handler.establish_connection(spec) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 88810cb328..c983bc0d93 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -99,7 +99,7 @@ module ActiveRecord ## # :singleton-method: # Specify whether schema dump should happen at the end of the - # db:migrate rake task. This is true by default, which is useful for the + # db:migrate rails command. This is true by default, which is useful for the # development environment. This should ideally be false in the production # environment where dumping schema is rarely needed. mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true @@ -139,11 +139,6 @@ module ActiveRecord end module ClassMethods # :nodoc: - def allocate - define_attribute_methods - super - end - def initialize_find_by_cache # :nodoc: @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end @@ -281,7 +276,7 @@ module ActiveRecord end def relation - relation = Relation.create(self, arel_table, predicate_builder) + relation = Relation.create(self) if finder_needs_type_condition? && !ignore_default_scope? relation.where!(type_condition) @@ -350,6 +345,28 @@ module ActiveRecord end ## + # Initializer used for instantiating objects that have been read from the + # database. +attributes+ should be an attributes object, and unlike the + # `initialize` method, no assignment calls are made per attribute. + # + # :nodoc: + def init_from_db(attributes) + init_internals + + @new_record = false + @attributes = attributes + + self.class.define_attribute_methods + + yield self if block_given? + + _run_find_callbacks + _run_initialize_callbacks + + self + end + + ## # :method: clone # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. # That means that modifying attributes of the clone will modify the original, since they will both point to the @@ -382,8 +399,10 @@ module ActiveRecord _run_initialize_callbacks - @new_record = true - @destroyed = false + @new_record = true + @destroyed = false + @_start_transaction_state = {} + @transaction_state = nil super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index ee4f818cbf..c7f0077a76 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -47,8 +47,12 @@ module ActiveRecord 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 - updates = { counter_name.to_sym => object.send(counter_association).count(:all) } - updates.merge!(touch_updates(touch)) if touch + updates = { counter_name => object.send(counter_association).count(:all) } + + if touch + names = touch if touch != true + updates.merge!(touch_attributes_with_time(*names)) + end unscoped.where(primary_key => object.id).update_all(updates) end @@ -68,8 +72,8 @@ module ActiveRecord # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # * <tt>:touch</tt> option - Touch timestamp columns when updating. - # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to - # touch that column or an array of symbols to touch just those ones. + # If attribute names are passed, they are updated along with updated_at/on + # attributes. # # ==== Examples # @@ -98,20 +102,7 @@ module ActiveRecord # # `updated_at` = '2016-10-13T09:59:23-05:00' # # WHERE id IN (10, 15) def update_counters(id, counters) - touch = counters.delete(:touch) - - updates = counters.map do |counter_name, value| - operator = value < 0 ? "-" : "+" - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" - end - - if touch - touch_updates = touch_updates(touch) - updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? - end - - unscoped.where(primary_key => id).update_all updates.join(", ") + unscoped.where!(primary_key => id).update_counters(counters) end # Increment a numeric field by one, via a direct SQL update. @@ -165,13 +156,6 @@ module ActiveRecord def decrement_counter(counter_name, id, touch: nil) update_counters(id, counter_name => -1, touch: touch) end - - private - def touch_updates(touch) - touch = timestamp_attributes_for_update_in_model if touch == true - touch_time = current_time_from_proper_timezone - Array(touch).map { |column| [ column, touch_time ] }.to_h - end end private diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb new file mode 100644 index 0000000000..ffeed45030 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module ActiveRecord + module DatabaseConfigurations # :nodoc: + class DatabaseConfig + attr_reader :env_name, :spec_name, :config + + def initialize(env_name, spec_name, config) + @env_name = env_name + @spec_name = spec_name + @config = config + end + end + + # Selects the config for the specified environment and specification name + # + # For example if passed :development, and :animals it will select the database + # under the :development and :animals configuration level + def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: + configs_for(environment, configs).find do |db_config| + db_config.spec_name == specification_name + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == env + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || config["url"] || config["adapter"] + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + end +end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 1a3e6e4d09..23ecb24542 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -141,10 +141,7 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :name, :mapping, :subtype end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index efcbd44776..f61bc7b9e8 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -111,7 +111,8 @@ module ActiveRecord class RecordNotUnique < WrappedDatabaseException end - # Raised when a record cannot be inserted or updated because it references a non-existent record. + # Raised when a record cannot be inserted or updated because it references a non-existent record, + # or when a record cannot be deleted because a parent record references it. class InvalidForeignKey < WrappedDatabaseException end @@ -120,13 +121,13 @@ module ActiveRecord def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) @adapter = adapter if table - msg = <<-EOM.strip_heredoc + msg = +<<~EOM Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). EOM else - msg = <<-EOM + msg = +<<~EOM There is a mismatch between the foreign key and primary key column types. Verify that the foreign key column type and the primary key of the associated table match types. EOM diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 86f13d75d5..6e0f1a0dfb 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -169,18 +169,18 @@ module ActiveRecord # self.use_transactional_tests = true # # test "godzilla" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # Foo.destroy_all - # assert Foo.all.empty? + # assert_empty Foo.all # end # # test "godzilla aftermath" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # end # end # - # If you preload your test database with all fixture data (probably in the rake task) and use - # transactional tests, then you may omit all fixtures declarations in your test cases since + # If you preload your test database with all fixture data (probably by running `rails db:fixtures:load`) + # and use transactional tests, then you may omit all fixtures declarations in your test cases since # all the data's already there and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to @@ -540,47 +540,38 @@ module ActiveRecord } unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - conn, - fs_name, - klass, - ::File.join(fixtures_directory, fs_name)) - end - - update_all_loaded_fixtures fixtures_map - - connection.transaction(requires_new: true) do - deleted_tables = Hash.new { |h, k| h[k] = Set.new } - fixture_sets.each do |fs| - conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows + fixtures_map = {} + + fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + conn, + fs_name, + klass, + ::File.join(fixtures_directory, fs_name)) + end - table_rows.each_key do |table| - unless deleted_tables[conn].include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" - end - deleted_tables[conn] << table - end + update_all_loaded_fixtures fixtures_map + fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } - table_rows.each do |fixture_set_name, rows| - conn.insert_fixtures(rows, fixture_set_name) - end + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - conn.reset_pk_sequence!(fs.table_name) - end + set.each do |fs| + fs.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) end end + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } + end end + + cache_fixtures(connection, fixtures_map) end cached_fixtures(connection, fixture_set_names) end @@ -883,6 +874,7 @@ module ActiveRecord class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances class_attribute :pre_loaded_fixtures, default: false class_attribute :config, default: ActiveRecord::Base + class_attribute :lock_threads, default: true end module ClassMethods @@ -982,7 +974,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads end # When connections are established in the future, begin a transaction too @@ -998,7 +990,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads @fixture_connections << connection end end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 7e47dac016..72035a986b 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -7,10 +7,10 @@ module ActiveRecord end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index f3fe610c09..6891c575c7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) - if subclass.nil? && base_class == self + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end end @@ -104,21 +104,53 @@ module ActiveRecord end end - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - # If you are using inheritance with ActiveRecord and don't want child classes - # to utilize the implied STI table name of the parent class, this will need to be true. - # For example, given the following: + # Returns whether the class is a base class. + # See #base_class for more information. + def base_class? + base_class == self + end + + # Set this to +true+ if this is an abstract class (see + # <tt>abstract_class?</tt>). + # If you are using inheritance with Active Record and don't want a class + # to be considered as part of the STI hierarchy, you must set this to + # true. + # +ApplicationRecord+, for example, is generated as an abstract class. + # + # Consider the following default behaviour: + # + # Shape = Class.new(ActiveRecord::Base) + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) + # + # Shape.table_name # => "shapes" + # Polygon.table_name # => "shapes" + # Square.table_name # => "shapes" + # Shape.create! # => #<Shape id: 1, type: nil> + # Polygon.create! # => #<Polygon id: 2, type: "Polygon"> + # Square.create! # => #<Square id: 3, type: "Square"> # - # class SuperClass < ActiveRecord::Base + # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from + # the hierarchy: + # + # class Shape < ActiveRecord::Base # self.abstract_class = true # end - # class Child < SuperClass - # self.table_name = 'the_table_i_really_want' - # end - # + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) # - # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # Shape.table_name # => nil + # Polygon.table_name # => "polygons" + # Square.table_name # => "polygons" + # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated. + # Polygon.create! # => #<Polygon id: 1, type: nil> + # Square.create! # => #<Square id: 2, type: "Square"> # + # Note that in the above example, to disallow the creation of a plain + # +Polygon+, you should use <tt>validates :type, presence: true</tt>, + # instead of setting it as an abstract class. This way, +Polygon+ will + # stay in the hierarchy, and Active Record will continue to correctly + # derive the table name. attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -130,6 +162,10 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def polymorphic_name + base_class.name + end + def inherited(subclass) subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) super diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 5a65edf27e..3626a13d7c 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -17,7 +17,7 @@ module ActiveRecord end def []=(key, value) - find_or_initialize_by(key: key).update_attributes!(value: value) + find_or_initialize_by(key: key).update!(value: value) end def [](key) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e1e24e2814..7f096bb532 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,13 +61,6 @@ module ActiveRecord end private - - def increment_lock - lock_col = self.class.locking_column - previous_lock_value = send(lock_col) - send("#{lock_col}=", previous_lock_value + 1) - end - def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect @@ -77,63 +70,56 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) + def _touch_row(attribute_names, time) + super + ensure + clear_attribute_change(self.class.locking_column) if locking_enabled? + end + + def _update_row(attribute_names, attempted_action = "update") return super unless locking_enabled? - return 0 if attribute_names.empty? begin - lock_col = self.class.locking_column - - previous_lock_value = read_attribute_before_type_cast(lock_col) - - increment_lock - - attribute_names.push(lock_col) + locking_column = self.class.locking_column + previous_lock_value = read_attribute_before_type_cast(locking_column) + attribute_names << locking_column - relation = self.class.unscoped + self[locking_column] += 1 - affected_rows = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value - ).update_all( - attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end.to_h + affected_rows = self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database, + locking_column => previous_lock_value ) - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, attempted_action) end affected_rows # If something went wrong, revert the locking_column value. rescue Exception - send("#{lock_col}=", previous_lock_value.to_i) - + self[locking_column] = previous_lock_value.to_i raise end end def destroy_row - affected_rows = super - - if locking_enabled? && affected_rows != 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + return super unless locking_enabled? - affected_rows - end + locking_column = self.class.locking_column - def relation_for_destroy - relation = super + affected_rows = self.class._delete_record( + self.class.primary_key => id_in_database, + locking_column => read_attribute_before_type_cast(locking_column) + ) - if locking_enabled? - locking_column = self.class.locking_column - relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") end - relation + affected_rows end module ClassMethods diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index bb85c47e06..5d1d15c94d 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -62,7 +62,7 @@ module ActiveRecord # the locked record. def lock!(lock = true) if persisted? - if changed? + if has_changes_to_save? raise(<<-MSG.squish) Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 9234029c22..1ae6840921 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -100,36 +100,21 @@ module ActiveRecord end def log_query_source - source_line, line_number = extract_callstack(caller_locations) + location = extract_query_source_location(caller_locations) - if source_line - if defined?(::Rails.root) - app_root = "#{::Rails.root.to_s}/".freeze - source_line = source_line.sub(app_root, "") - end + if location + source = "#{location.path}:#{location.lineno}" + source = source.sub("#{::Rails.root}/", "") if defined?(::Rails.root) - logger.debug(" ↳ #{ source_line }:#{ line_number }") + logger.debug(" ↳ #{source}") end end - def extract_callstack(callstack) - line = callstack.find do |frame| - frame.absolute_path && !ignored_callstack(frame.absolute_path) - end - - offending_line = line || callstack.first - - [ - offending_line.path, - offending_line.lineno - ] - end - - RAILS_GEM_ROOT = File.expand_path("../../../..", __FILE__) + "/" + RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/" + PATHS_TO_IGNORE = /\A(#{RAILS_GEM_ROOT}|#{RbConfig::CONFIG["rubylibdir"]})/ - def ignored_callstack(path) - path.start_with?(RAILS_GEM_ROOT) || - path.start_with?(RbConfig::CONFIG["rubylibdir"]) + def extract_query_source_location(locations) + locations.find { |line| line.absolute_path && !line.absolute_path.match?(PATHS_TO_IGNORE) } end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index f6648a4e3d..d68ed3cad9 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" @@ -129,9 +130,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}") + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate") + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate") else super end @@ -140,6 +141,7 @@ module ActiveRecord class ConcurrentMigrationError < MigrationError #:nodoc: DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze def initialize(message = DEFAULT_MESSAGE) super @@ -148,7 +150,7 @@ module ActiveRecord class NoEnvironmentInSchemaError < MigrationError #:nodoc: def initialize - msg = "Environment data not found in the schema. To resolve this issue, run: \n\n bin/rails db:environment:set" + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}") else @@ -171,7 +173,7 @@ module ActiveRecord msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" - msg << " bin/rails db:environment:set" + msg << " rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else @@ -350,7 +352,7 @@ module ActiveRecord # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also - # invoke the db:schema:dump task, which will update your db/schema.rb file + # invoke the db:schema:dump command, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use @@ -550,7 +552,7 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i if @last_check < mtime ActiveRecord::Migration.check_pending!(connection) @last_check = mtime @@ -575,11 +577,11 @@ module ActiveRecord # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root FileUtils.cd(root) do @@ -829,10 +831,14 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end + # Takes a message argument and outputs it as is. + # A second boolean argument can be passed to specify whether to indent or not. def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end + # Outputs text along with how long it took to run its block. + # If the block returns an integer it assumes it is the number of rows affected. def say_with_time(message) say(message) result = nil @@ -842,6 +848,7 @@ module ActiveRecord result end + # Takes a block as an argument and suppresses any output generated by the block. def suppress_messages save, self.verbose = verbose, false yield @@ -876,10 +883,10 @@ module ActiveRecord FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::Migrator.migrations(destination) + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + source_migrations = ActiveRecord::MigrationContext.new(path).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -997,132 +1004,147 @@ module ActiveRecord end end - class Migrator#:nodoc: - class << self - attr_writer :migrations_paths - alias :migrations_path= :migrations_paths= - - def migrate(migrations_paths, target_version = nil, &block) - case - when target_version.nil? - up(migrations_paths, target_version, &block) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version, &block) - else - up(migrations_paths, target_version, &block) - end - end + class MigrationContext # :nodoc: + attr_reader :migrations_paths - def rollback(migrations_paths, steps = 1) - move(:down, migrations_paths, steps) - end + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end - def forward(migrations_paths, steps = 1) - move(:up, migrations_paths, steps) + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) end + end - def up(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + def rollback(steps = 1) + move(:down, steps) + end - new(:up, migrations, target_version).migrate + def forward(steps = 1) + move(:up, steps) + end + + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def down(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + Migrator.new(:up, selected_migrations, target_version).migrate + end - new(:down, migrations, target_version).migrate + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def run(direction, migrations_paths, target_version) - new(direction, migrations(migrations_paths), target_version).run - end + Migrator.new(:down, selected_migrations, target_version).migrate + end - def open(migrations_paths) - new(:up, migrations(migrations_paths), nil) - end + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end - def get_all_versions - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) - else - [] - end - end + def open + Migrator.new(:up, migrations, nil) + end - def current_version(connection = nil) - get_all_versions.max || 0 - rescue ActiveRecord::NoDatabaseError + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end + end - def needs_migration?(connection = nil) - (migrations(migrations_paths).collect(&:version) - get_all_versions).size > 0 - end + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end - def any_migrations? - migrations(migrations_paths).any? - end + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end - def last_migration #:nodoc: - migrations(migrations_paths).last || NullMigration.new - end + def any_migrations? + migrations.any? + end - def migrations_paths - @migrations_paths ||= ["db/migrate"] - # just to not break things if someone uses: migrations_path = some_string - Array(@migrations_paths) - end + def last_migration #:nodoc: + migrations.last || NullMigration.new + end - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + MigrationProxy.new(name, version, file, scope) end - def migrations(paths) - paths = Array(paths) + migrations.sort_by(&:version) + end - migrations = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - name = name.camelize + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions - MigrationProxy.new(name, version, file, scope) - end + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact - migrations.sort_by(&:version) + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] end - def migrations_status(paths) - paths = Array(paths) - - db_list = ActiveRecord::SchemaMigration.normalized_versions + (db_list + file_list).sort_by { |_, version, _| version } + end - file_list = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end - (db_list + file_list).sort_by { |_, version, _| version } - end + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end - def migration_files(paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - private + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end - def move(direction, migrations_paths, steps) - migrator = new(direction, migrations(migrations_paths)) + private + def move(direction, steps) + migrator = Migrator.new(direction, migrations) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1137,10 +1159,29 @@ module ActiveRecord finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." + self.migrations_paths = [path] + end + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version end end + self.migrations_paths = ["db/migrate"] + def initialize(direction, migrations, target_version = nil) @direction = direction @target_version = target_version @@ -1203,7 +1244,7 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(self.class.get_all_versions) + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) end private @@ -1235,7 +1276,7 @@ module ActiveRecord # Stores the current environment in the database. def record_environment return if down? - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end def ran?(migration) @@ -1294,23 +1335,6 @@ module ActiveRecord end end - def self.last_stored_environment - return nil if current_version == 0 - raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - - environment = ActiveRecord::InternalMetadata[:environment] - raise NoEnvironmentInSchemaError unless environment - environment - end - - def self.current_environment - ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - end - - def self.protected_environment? - ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment - end - def up? @direction == :up end @@ -1338,12 +1362,17 @@ module ActiveRecord def with_advisory_lock lock_id = generate_migrator_advisory_lock_id - got_lock = Base.connection.get_advisory_lock(lock_id) + connection = Base.connection + got_lock = connection.get_advisory_lock(lock_id) raise ConcurrentMigrationError unless got_lock load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield ensure - Base.connection.release_advisory_lock(lock_id) if got_lock + if got_lock && !connection.release_advisory_lock(lock_id) + raise ConcurrentMigrationError.new( + ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE + ) + end end MIGRATOR_SALT = 2053462845 diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 81ef4828f8..087632b10f 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -85,7 +85,7 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + raise IrreversibleMigration, <<~MSG unless respond_to?(method, true) This migration uses #{command}, which is not automatically reversible. To make the migration reversible you can either: 1. Define #up and #down methods in place of the #change method. diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 7ae8073478..0edaaa0cf9 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,7 +13,10 @@ module ActiveRecord const_get(name) end - V5_2 = Current + V6_0 = Current + + class V5_2 < V6_0 + end class V5_1 < V5_2 def change_column(table_name, column_name, type, options = {}) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index fa7537c1a3..694ff85fa1 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -276,7 +276,7 @@ module ActiveRecord end def sequence_name - if base_class == self + if base_class? @sequence_name ||= reset_sequence_name else (@sequence_name ||= nil) || base_class.sequence_name @@ -361,8 +361,9 @@ module ActiveRecord # it). # # +attr_name+ The name of the attribute to retrieve the type for. Must be - # a string + # a string or a symbol. def type_for_attribute(attr_name, &block) + attr_name = attr_name.to_s if block attribute_types.fetch(attr_name, &block) else @@ -378,6 +379,7 @@ module ActiveRecord end def _default_attributes # :nodoc: + load_schema @default_attributes ||= ActiveModel::AttributeSet.new({}) end @@ -499,8 +501,7 @@ module ActiveRecord # Computes and returns a table name according to default conventions. def compute_table_name - base = base_class - if self == base + if base_class? # Nested classes are prefixed with singular parent table name. if parent < Base && !parent.abstract_class? contained = parent.table_name @@ -511,7 +512,7 @@ module ActiveRecord "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. - base.table_name + base_class.table_name end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index fa20bce3a9..50767ee93f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -501,7 +501,7 @@ module ActiveRecord if attributes["id"].blank? unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*UNASSIGNABLE_KEYS)) + association.reader.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s } unless call_reject_if(association_name, attributes) diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index 754c891884..697076bdae 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -43,6 +43,13 @@ module ActiveRecord end end + # Returns +true+ if the class has +no_touching+ set, +false+ otherwise. + # + # Project.no_touching do + # Project.first.no_touching? # true + # Message.first.no_touching? # false + # end + # def no_touching? NoTouching.applied_to?(self.class) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 462e5e7aaf..05963e5546 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -67,8 +67,18 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}, &block) klass = discriminate_class_for_record(attributes) + instantiate_instance_of(klass, attributes, column_types, &block) + end + + # Given a class, an attributes hash, +instantiate_instance_of+ returns a + # new instance of the class. Accepts only keys as strings. + # + # This is private, don't call it. :) + # + # :nodoc: + def instantiate_instance_of(klass, attributes, column_types = {}, &block) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) + klass.allocate.init_from_db(attributes, &block) end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -97,13 +107,11 @@ module ActiveRecord # When running callbacks is not needed for each record update, # it is preferred to use {update_all}[rdoc-ref:Relation#update_all] # for updating all records in a single query. - def update(id = :all, attributes) + def update(id, attributes) if id.is_a?(Array) id.map { |one_id| find(one_id) }.each_with_index { |object, idx| object.update(attributes[idx]) } - elsif id == :all - all.each { |record| record.update(attributes) } else if ActiveRecord::Base === id raise ArgumentError, @@ -169,17 +177,16 @@ module ActiveRecord primary_key_value = nil if primary_key && Hash === values - arel_primary_key = arel_attribute(primary_key) - primary_key_value = values[arel_primary_key] + primary_key_value = values[primary_key] if !primary_key_value && prefetch_primary_key? primary_key_value = next_sequence_value - values[arel_primary_key] = primary_key_value + values[primary_key] = primary_key_value end end if values.empty? - im = arel_table.compile_insert(connection.empty_insert_statement_value) + im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key)) im.into arel_table else im = arel_table.compile_insert(_substitute_values(values)) @@ -188,15 +195,26 @@ module ActiveRecord connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) end - def _update_record(values, id, id_was) # :nodoc: - bind = predicate_builder.build_bind_attribute(primary_key, id_was || id) + def _update_record(values, constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + um = arel_table.where( - arel_attribute(primary_key).eq(bind) + constraints.reduce(&:and) ).compile_update(_substitute_values(values), primary_key) connection.update(um, "#{self} Update") end + def _delete_record(constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + + dm = Arel::DeleteManager.new + dm.from(arel_table) + dm.wheres = constraints + + connection.delete(dm, "#{self} Destroy") + end + private # Called by +instantiate+ to decide which class to use for a new # record instance. @@ -208,8 +226,9 @@ module ActiveRecord end def _substitute_values(values) - values.map do |attr, value| - bind = predicate_builder.build_bind_attribute(attr.name, value) + values.map do |name, value| + attr = arel_attribute(name) + bind = predicate_builder.build_bind_attribute(name, value) [attr, bind] end end @@ -310,7 +329,7 @@ module ActiveRecord # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - _relation_for_itself.delete_all if persisted? + _delete_row if persisted? @destroyed = true freeze end @@ -359,9 +378,10 @@ module ActiveRecord # Any change to the attributes on either instance will affect both instances. # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) - became = klass.new + became = klass.allocate + became.send(:initialize) became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) + became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -418,6 +438,7 @@ module ActiveRecord end alias update_attributes update + deprecate :update_attributes # Updates its receiver just like #update but calls #save! instead # of +save+, so an exception is raised if the record is invalid and saving will fail. @@ -431,6 +452,7 @@ module ActiveRecord end alias update_attributes! update! + deprecate :update_attributes! # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) @@ -461,13 +483,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end - updated_count = _relation_for_itself.update_all(attributes) + affected_rows = self.class._update_record( + attributes, + self.class.primary_key => id_in_database + ) attributes.each do |k, v| write_attribute_without_type_cast(k, v) end - updated_count == 1 + affected_rows == 1 end # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). @@ -640,35 +665,12 @@ module ActiveRecord MSG end - time ||= current_time_from_proper_timezone - attributes = timestamp_attributes_for_update_in_model - attributes.concat(names) - - unless attributes.empty? - changes = {} - - attributes.each do |column| - column = column.to_s - changes[column] = write_attribute(column, time) - end - - scope = _relation_for_itself - - if locking_enabled? - locking_column = self.class.locking_column - scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column)) - changes[locking_column] = increment_lock - end - - clear_attribute_changes(changes.keys) - result = scope.update_all(changes) == 1 + attribute_names = timestamp_attributes_for_update_in_model + attribute_names |= names.map(&:to_s) - if !result && locking_enabled? - raise ActiveRecord::StaleObjectError.new(self, "touch") - end - - @_trigger_update_callback = result - result + unless attribute_names.empty? + affected_rows = _touch_row(attribute_names, time) + @_trigger_update_callback = affected_rows == 1 else true end @@ -681,19 +683,34 @@ module ActiveRecord end def destroy_row - relation_for_destroy.delete_all + _delete_row end - def relation_for_destroy - _relation_for_itself + def _delete_row + self.class._delete_record(self.class.primary_key => id_in_database) end - def _relation_for_itself - self.class.unscoped.where(self.class.primary_key => id) + def _touch_row(attribute_names, time) + time ||= current_time_from_proper_timezone + + attribute_names.each do |attr_name| + write_attribute(attr_name, time) + clear_attribute_change(attr_name) + end + + _update_row(attribute_names, "touch") + end + + def _update_row(attribute_names, attempted_action = "update") + self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database + ) end def create_or_update(*args, &block) _raise_readonly_record_error if readonly? + return false if destroyed? result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -701,24 +718,27 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_values.empty? - rows_affected = 0 + attribute_names &= self.class.column_names + attribute_names = attributes_for_update(attribute_names) + + if attribute_names.empty? + affected_rows = 0 @_trigger_update_callback = true else - rows_affected = self.class._update_record(attributes_values, id, id_in_database) - @_trigger_update_callback = rows_affected > 0 + affected_rows = _update_row(attribute_names) + @_trigger_update_callback = affected_rows == 1 end yield(self) if block_given? - rows_affected + affected_rows end # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_create(attribute_names) + attribute_names &= self.class.column_names + attributes_values = attributes_with_values_for_create(attribute_names) new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 8e23128333..28194c7c46 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -7,38 +7,31 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.cache(&block) + else + yield end end # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.uncached(&block) + else + yield end end end def self.run - ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool| - caching_was_enabled = pool.query_cache_enabled - - pool.enable_query_cache! - - [pool, caching_was_enabled] - end + ActiveRecord::Base.connection_handler.connection_pool_list. + reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } end - def self.complete(caching_pools) - caching_pools.each do |pool, caching_was_enabled| - pool.disable_query_cache! unless caching_was_enabled - end + def self.complete(pools) + pools.each { |pool| pool.disable_query_cache! } ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 3996d5661f..c84f3d0fbb 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -5,7 +5,7 @@ module ActiveRecord delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all - delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all + delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy_all, :delete_all, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all @@ -13,7 +13,7 @@ module ActiveRecord :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all - delegate :pluck, :ids, to: :all + delegate :pluck, :pick, :ids, to: :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call @@ -49,7 +49,12 @@ module ActiveRecord } message_bus.instrument("instantiation.active_record", payload) do - result_set.map { |record| instantiate(record, column_types, &block) } + if result_set.includes_column?(inheritance_column) + result_set.map { |record| instantiate(record, column_types, &block) } + else + # Instantiate a homogeneous set + result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) } + end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 4538ed6a5f..009f412234 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -59,6 +59,7 @@ module ActiveRecord console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + ActiveRecord::Base.verbose_query_logs = false end runner do @@ -91,6 +92,7 @@ module ActiveRecord if File.file?(filename) current_version = ActiveRecord::Migrator.current_version + next if current_version.nil? cache = YAML.load(File.read(filename)) @@ -139,8 +141,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rails db:create` to create the database. - 3. Run `bin/rails db:setup` to load your database schema. + 2. Run `rails db:create` to create the database. + 3. Run `rails db:setup` to load your database schema. end_warning raise end @@ -155,6 +157,13 @@ end_warning end end + initializer "active_record.collection_cache_association_loading" do + require "active_record/railties/collection_cache_association_loading" + ActiveSupport.on_load(:action_view) do + ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + end + end + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do ActiveSupport::Reloader.before_class_unload do diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb new file mode 100644 index 0000000000..b5129e4239 --- /dev/null +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveRecord + module Railties # :nodoc: + module CollectionCacheAssociationLoading #:nodoc: + def setup(context, options, block) + @relation = relation_from_options(options) + + super + end + + def relation_from_options(cached: nil, partial: nil, collection: nil, **_) + return unless cached + + relation = partial if partial.is_a?(ActiveRecord::Relation) + relation ||= collection if collection.is_a?(ActiveRecord::Relation) + + if relation && !relation.loaded? + relation.skip_preloading! + end + end + + def collection_without_template + @relation.preload_associations(@collection) if @relation + super + end + + def collection_with_template + @relation.preload_associations(@collection) if @relation + super + end + end + end +end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 2ae733f657..309441a057 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -8,49 +8,44 @@ module ActiveRecord module ControllerRuntime #:nodoc: extend ActiveSupport::Concern - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_internal :db_runtime - - private - - def process_action(action, *args) - # We also need to reset the runtime before each action - # because of queries in middleware or in cases we are streaming - # and it won't be cleaned up by the method below. - ActiveRecord::LogSubscriber.reset_runtime - super + module ClassMethods # :nodoc: + def log_process_action(payload) + messages, db_runtime = super, payload[:db_runtime] + messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime + messages + end end - def cleanup_view_runtime - if logger && logger.info? && ActiveRecord::Base.connected? - db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime = (db_runtime || 0) + db_rt_before_render - runtime = super - db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime += db_rt_after_render - runtime - db_rt_after_render - else + private + attr_internal :db_runtime + + def process_action(action, *args) + # We also need to reset the runtime before each action + # because of queries in middleware or in cases we are streaming + # and it won't be cleaned up by the method below. + ActiveRecord::LogSubscriber.reset_runtime super end - end - def append_info_to_payload(payload) - super - if ActiveRecord::Base.connected? - payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + def cleanup_view_runtime + if logger && logger.info? && ActiveRecord::Base.connected? + db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime = (db_runtime || 0) + db_rt_before_render + runtime = super + db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime += db_rt_after_render + runtime - db_rt_after_render + else + super + end end - end - module ClassMethods # :nodoc: - def log_process_action(payload) - messages, db_runtime = super, payload[:db_runtime] - messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime - messages + def append_info_to_payload(payload) + super + if ActiveRecord::Base.connected? + payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + end end - end end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index fce3e1c5cf..8b7d18fb3d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -6,7 +6,7 @@ db_namespace = namespace :db do desc "Set the environment value for the database" task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end task check_protected_environments: :load_config do @@ -22,6 +22,14 @@ db_namespace = namespace :db do task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Create #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + end + end end desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases." @@ -33,6 +41,14 @@ db_namespace = namespace :db do task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Drop #{spec_name} database for current environment" + task spec_name => [:load_config, :check_protected_environments] do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end + end end desc "Drops 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 or when RAILS_ENV is development, it defaults to dropping the development and test databases." @@ -57,7 +73,10 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Tasks::DatabaseTasks.migrate + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end db_namespace["_dump"].invoke end @@ -77,6 +96,15 @@ db_namespace = namespace :db do end namespace :migrate do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Migrate #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? @@ -99,9 +127,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :up, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -113,9 +140,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :down, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -131,8 +157,7 @@ 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 - paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths - ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -142,14 +167,14 @@ db_namespace = namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.rollback(step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.forward(step) db_namespace["_dump"].invoke end @@ -172,12 +197,12 @@ db_namespace = namespace :db do desc "Retrieves the current schema version number" task version: :load_config do - puts "Current version: #{ActiveRecord::Migrator.current_version}" + puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}" end # desc "Raises an error if there are pending migrations" task abort_if_pending_migrations: :load_config do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -192,7 +217,7 @@ db_namespace = namespace :db do task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed] desc "Loads the seed data from db/seeds.rb" - task :seed do + task seed: :load_config do db_namespace["abort_if_pending_migrations"].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end @@ -232,7 +257,7 @@ db_namespace = namespace :db do base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| - if data = YAML::load(ERB.new(IO.read(file)).result) + if data = YAML.load(ERB.new(IO.read(file)).result) data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) @@ -249,10 +274,15 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") - File.open(filename, "w:utf-8") do |file| - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + File.open(filename, "w:utf-8") do |file| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end end + db_namespace["schema:dump"].reenable end @@ -279,22 +309,24 @@ db_namespace = namespace :db do rm_f filename, verbose: false end end - end namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) + + if ActiveRecord::SchemaMigration.table_exists? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" + end end end + db_namespace["structure:dump"].reenable end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 9e32b69786..6d2f75a3ae 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,33 +13,37 @@ module ActiveRecord class_attribute :aggregate_reflections, instance_writer: false, default: {} end - def self.create(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 + class << self + def create(macro, name, scope, options, ar) + reflection = reflection_class_for(macro).new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection + end - reflection = klass.new(name, scope, options, ar) - options[:through] ? ThroughReflection.new(reflection) : reflection - end + def add_reflection(ar, name, reflection) + ar.clear_reflections_cache + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) + end - def self.add_reflection(ar, name, reflection) - ar.clear_reflections_cache - name = name.to_s - ar._reflections = ar._reflections.except(name).merge!(name => reflection) - end + def add_aggregate_reflection(ar, name, reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) + end - def self.add_aggregate_reflection(ar, name, reflection) - ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) + private + def reflection_class_for(macro) + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + end end # \Reflection enables the ability to examine the associations and aggregations of @@ -193,7 +197,7 @@ module ActiveRecord klass_scope = klass_join_scope(table, predicate_builder) if type - klass_scope.where!(type => foreign_klass.base_class.sti_name) + klass_scope.where!(type => foreign_klass.polymorphic_name) end scope_chain_items.inject(klass_scope, &:merge!) @@ -291,7 +295,11 @@ module ActiveRecord end def build_scope(table, predicate_builder = predicate_builder(table)) - Relation.create(klass, table, predicate_builder) + Relation.create( + klass, + table: table, + predicate_builder: predicate_builder + ) end def join_primary_key(*) @@ -412,6 +420,9 @@ module ActiveRecord # Active Record class. class AssociationReflection < MacroReflection #:nodoc: def compute_class(name) + if polymorphic? + raise ArgumentError, "Polymorphic associations do not support computing the class." + end active_record.send(:compute_type, name) end @@ -564,7 +575,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key] def add_as_source(seed) seed @@ -622,9 +633,6 @@ module ActiveRecord # +automatic_inverse_of+ method is a valid reflection. We must # make sure that the reflection's active_record name matches up # with the current reflection's klass name. - # - # Note: klass will always be valid because when there's a NameError - # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && klass <= reflection.active_record && @@ -728,6 +736,9 @@ module ActiveRecord end private + def can_find_inverse_of_automatically?(_) + !polymorphic? && super + end def calculate_constructable(macro, options) !polymorphic? @@ -958,16 +969,14 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :delegate_reflection - def actual_source_reflection # FIXME: this is a horrible name source_reflection.actual_source_reflection end private + attr_reader :delegate_reflection + def collect_join_reflections(seed) a = source_reflection.add_as_source seed if options[:source_type] diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 4df3864d07..2d3e1eaa08 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -9,6 +9,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] @@ -18,17 +19,19 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder + attr_accessor :skip_preloading_value alias :model :klass alias :loaded? :loaded alias :locked? :lock_value - def initialize(klass, table, predicate_builder, values = {}) + def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false @predicate_builder = predicate_builder + @delegate_to_klass = false end def initialize_copy(other) @@ -40,6 +43,12 @@ module ActiveRecord klass.arel_attribute(name, table) end + def bind_attribute(name, value) # :nodoc: + attr = arel_attribute(name) + bind = predicate_builder.build_bind_attribute(attr.name, value) + yield attr, bind + end + # Initializes new record from relation while maintaining the current # scope. # @@ -142,23 +151,12 @@ module ActiveRecord # failed due to validation errors it won't be persisted, you get what # #create returns in such situation. # - # Please note *this method is not atomic*, it runs first a SELECT, and if + # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads # or processes there is a race condition between both calls and it could # be the case that you end up with two similar records. # - # Whether that is a problem or not depends on the logic of the - # application, but in the particular case in which rows have a UNIQUE - # constraint an exception may be raised, just retry: - # - # begin - # CreditAccount.transaction(requires_new: true) do - # CreditAccount.find_or_create_by(user_id: user.id) - # end - # rescue ActiveRecord::RecordNotUnique - # retry - # end - # + # If this might be a problem for your application, please see #create_or_find_by. def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end @@ -170,6 +168,47 @@ module ActiveRecord find_by(attributes) || create!(attributes, &block) end + # Attempts to create a record with the given attributes in a table that has a unique constraint + # on one or several of its columns. If a row already exists with one or several of these + # unique constraints, the exception such an insertion would normally raise is caught, + # and the existing record with those attributes is found using #find_by. + # + # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT + # and the INSERT, as that method needs to first query the table, then attempt to insert a row + # if none is found. + # + # There are several drawbacks to #create_or_find_by, though: + # + # * The underlying table must have the relevant columns defined with unique constraints. + # * A unique constraint violation may be triggered by only one, or at least less than all, + # of the given attributes. This means that the subsequent #find_by may fail to find a + # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception, + # rather than a record with the given attributes. + # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by, + # we actually have another race condition between INSERT -> SELECT, which can be triggered + # if a DELETE between those two statements is run by another client. But for most applications, + # that's a significantly less likely condition to hit. + # * It relies on exception handling to handle control flow, which may be marginally slower. + # + # This method will return a record if all given attributes are covered by unique constraints + # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted + # and failed due to validation errors it won't be persisted, you get what #create returns in + # such situation. + def create_or_find_by(attributes, &block) + transaction(requires_new: true) { create(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + + # Like #create_or_find_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception + # is raised if the created record is invalid. + def create_or_find_by!(attributes, &block) + transaction(requires_new: true) { create!(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) @@ -184,7 +223,7 @@ module ActiveRecord # are needed by the next ones when eager loading is going on. # # Please see further details in the - # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. + # {Active Record Query Interface guide}[https://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -276,10 +315,17 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope(true), self + previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass yield ensure - klass.current_scope = previous + klass.current_scope = previous unless @delegate_to_klass + end + + def _exec_scope(*args, &block) # :nodoc: + @delegate_to_klass = true + instance_exec(*args, &block) || self + ensure + @delegate_to_klass = false end # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE @@ -329,6 +375,62 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end + def update(attributes) # :nodoc: + each { |record| record.update(attributes) } + end + + def update_counters(counters) # :nodoc: + touch = counters.delete(:touch) + + updates = counters.map do |counter_name, value| + operator = value < 0 ? "-" : "+" + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end + + if touch + names = touch if touch != true + touch_updates = klass.touch_attributes_with_time(*names) + updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + end + + update_all updates.join(", ") + end + + # Touches all records in the current relation without instantiating records first with the updated_at/on attributes + # set to the current time or the time specified. + # This method can be passed attribute names and an optional time argument. + # If attribute names are passed, they are updated along with updated_at/on attributes. + # If no time argument is passed, the current time is used as default. + # + # === Examples + # + # # Touch all records + # Person.all.touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a custom attribute + # Person.all.touch_all(:created_at) + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a specified time + # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) + # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'" + # + # # Touch records with scope + # Person.where(name: 'David').touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" + def touch_all(*names, time: nil) + updates = touch_attributes_with_time(*names, time: time) + + if klass.locking_enabled? + quoted_locking_column = connection.quote_column_name(klass.locking_column) + updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1" + end + + update_all(updates) + end + # Destroys the records by instantiating each # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. # Each object's callbacks are executed (including <tt>:dependent</tt> association options). @@ -415,6 +517,7 @@ module ActiveRecord end def reset + @delegate_to_klass = false @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} @@ -427,17 +530,16 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin - relation = self - - if eager_loading? - find_with_associations { |rel, _| relation = rel } - end - - conn = klass.connection - conn.unprepared_statement { - conn.to_sql(relation.arel) - } - end + if eager_loading? + apply_join_dependency do |relation, join_dependency| + relation = join_dependency.apply_column_aliases(relation) + relation.to_sql + end + else + conn = klass.connection + conn.unprepared_statement { conn.to_sql(arel) } + end + end end # Returns a hash of where conditions. @@ -516,6 +618,16 @@ module ActiveRecord ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) end + def preload_associations(records) # :nodoc: + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload records, associations + end + end + protected def load_records(records) @@ -533,10 +645,11 @@ module ActiveRecord skip_query_cache_if_necessary do @records = if eager_loading? - find_with_associations do |relation, join_dependency| + apply_join_dependency do |relation, join_dependency| if ActiveRecord::NullRelation === relation [] else + relation = join_dependency.apply_column_aliases(relation) rows = connection.select_all(relation.arel, "SQL") join_dependency.instantiate(rows, &block) end.freeze @@ -545,13 +658,7 @@ module ActiveRecord klass.find_by_sql(arel, &block).freeze end - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + preload_associations(@records) unless skip_preloading_value @records.each(&:readonly!) if readonly_value diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 561869017a..9c579843b1 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -251,25 +251,28 @@ module ActiveRecord end end - attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key)) - batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr))) + batch_relation = relation.where( + bind_attribute(primary_key, primary_key_offset) { |attr, bind| attr.gt(bind) } + ) end end private def apply_limits(relation, start, finish) - if start - attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr))) - end - if finish - attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr))) - end + relation = apply_start_limit(relation, start) if start + relation = apply_finish_limit(relation, finish) if finish relation end + def apply_start_limit(relation, start) + relation.where(bind_attribute(primary_key, start) { |attr, bind| attr.gteq(bind) }) + end + + def apply_finish_limit(relation, finish) + relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) }) + end + def batch_order arel_attribute(primary_key).asc end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index cb0b06cfdc..40fe39fa9d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -131,7 +131,14 @@ module ActiveRecord def calculate(operation, column_name) if has_include?(column_name) relation = apply_join_dependency - relation.distinct! if operation.to_s.downcase == "count" + + if operation.to_s.downcase == "count" + relation.distinct! + # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT + if (column_name == :all || column_name.nil?) && select_values.empty? + relation.order_values = [] + end + end relation.calculate(operation, column_name) else @@ -193,6 +200,24 @@ module ActiveRecord end end + # Pick the value(s) from the named column(s) in the current relation. + # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful + # when you have a relation that's already narrowed down to a single row. + # + # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also + # more efficient. The value is, again like with pluck, typecast by the column type. + # + # Person.where(id: 1).pick(:name) + # # SELECT people.name FROM people WHERE id = 1 LIMIT 1 + # # => 'David' + # + # Person.where(id: 1).pick(:name, :email_address) + # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1 + # # => [ 'David', 'david@loudthinking.com' ] + def pick(*column_names) + limit(1).pluck(*column_names).first + end + # Pluck all the ID's for the relation using the table's primary key # # Person.ids # SELECT people.id FROM people @@ -220,7 +245,7 @@ module ActiveRecord if distinct && (group_values.any? || select_values.empty? && order_values.empty?) column_name = primary_key end - elsif column_name =~ /\s*DISTINCT[\s(]+/i + elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) distinct = nil end end @@ -235,7 +260,7 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 4863befec8..488f71cdde 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -38,7 +38,7 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :each, :uniq, :join, + delegate :to_xml, :encode_with, :length, :each, :join, :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json, :shuffle, :split, :slice, :index, :rindex, to: :records @@ -82,6 +82,11 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } + elsif @delegate_to_klass && @klass.respond_to?(method, true) + ActiveSupport::Deprecation.warn \ + "Delegating missing #{method} method to #{@klass}. " \ + "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." + @klass.send(method, *args, &block) elsif arel.respond_to?(method) ActiveSupport::Deprecation.warn \ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index ff06ecbee1..c5562c1ff0 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -148,7 +148,7 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - return find_last(limit) if loaded? || limit_value + return find_last(limit) if loaded? || has_limit_or_offset? result = ordered_relation.limit(limit) result = result.reverse_order! @@ -313,7 +313,7 @@ module ActiveRecord return false if !conditions || limit_value == 0 if eager_loading? - relation = apply_join_dependency(construct_join_dependency(eager_loading: false)) + relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) end @@ -358,24 +358,6 @@ module ActiveRecord offset_value || 0 end - def find_with_associations - # 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 - - relation = apply_join_dependency(join_dependency) - relation._select!(join_dependency.aliases.columns) - - yield relation, join_dependency - end - def construct_relation_for_exists(conditions) relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) @@ -389,24 +371,28 @@ module ActiveRecord relation end - def construct_join_dependency(eager_loading: true) - including = eager_load_values + includes_values + def construct_join_dependency(associations) ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins_values), eager_loading: eager_loading + klass, table, associations ) end - def apply_join_dependency(join_dependency = construct_join_dependency) + def apply_join_dependency(eager_loading: group_values.empty?) + join_dependency = construct_join_dependency(eager_load_values + includes_values) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value + if eager_loading && !using_limitable_reflections?(join_dependency.reflections) + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end - relation.except(:limit, :offset) + relation.limit_value = relation.offset_value = nil + end + + if block_given? + yield relation, join_dependency + else + relation end end @@ -532,7 +518,11 @@ module ActiveRecord else relation = ordered_relation - if limit_value.nil? || index < limit_value + if limit_value + limit = [limit_value - index, limit].min + end + + if limit > 0 relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a else @@ -547,12 +537,11 @@ module ActiveRecord else relation = ordered_relation - relation.to_a[-index] - # TODO: can be made more performant on large result sets by - # for instance, last(index)[-index] (which would require - # refactoring the last(n) finder method to make test suite pass), - # or by using a combination of reverse_order, limit, and offset, - # e.g., reverse_order.offset(index-1).first + if equal?(relation) || has_limit_or_offset? + relation.records[-index] + else + relation.last(index)[-index] + end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index b736b21525..10e4779ca4 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -23,7 +23,11 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table, relation.predicate_builder) + other = Relation.create( + relation.klass, + table: relation.table, + predicate_builder: relation.predicate_builder + ) hash.each { |k, v| if k == :joins if Hash === v @@ -52,7 +56,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -79,6 +83,7 @@ module ActiveRecord merge_clauses merge_preloads merge_joins + merge_outer_joins relation end @@ -112,14 +117,10 @@ module ActiveRecord if other.klass == relation.klass relation.joins!(*other.joins_values) else - alias_tracker = nil joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - alias_tracker ||= other.alias_tracker - ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join, alias_tracker - ) + other.send(:construct_join_dependency, join) else join end @@ -129,6 +130,25 @@ module ActiveRecord end end + def merge_outer_joins + return if other.left_outer_joins_values.blank? + + if other.klass == relation.klass + relation.left_outer_joins!(*other.left_outer_joins_values) + else + joins_dependency = other.left_outer_joins_values.map do |join| + case join + when Hash, Symbol, Array + other.send(:construct_join_dependency, join) + else + join + end + end + + relation.left_outer_joins!(*joins_dependency) + end + end + def merge_multi_values if other.reordering_value # override any order specified in the original relation diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 885c26d7aa..f734cd0ad8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -48,7 +48,12 @@ module ActiveRecord end def build(attribute, value) - handler_for(value).call(attribute, value) + if table.type(attribute.name).force_equality?(value) + bind = build_bind_attribute(attribute.name, value) + attribute.eq(bind) + else + handler_for(value).call(attribute, value) + end end def build_bind_attribute(column_name, value) @@ -57,9 +62,6 @@ module ActiveRecord end protected - - attr_reader :table - def expand_from_hash(attributes) return ["1=0"] if attributes.empty? @@ -86,10 +88,18 @@ module ActiveRecord expand_from_hash(query).reduce(&:and) end queries.reduce(&:or) - # FIXME: Deprecate this and provide a public API to force equality - elsif (value.is_a?(Range) || value.is_a?(Array)) && - table.type(key.to_s).respond_to?(:subtype) - BasicObjectHandler.new(self).call(table.arel_attribute(key), value) + elsif table.aggregated_with?(key) + mapping = table.reflect_on_aggregation(key).mapping + queries = Array.wrap(value).map do |object| + mapping.map do |field_attr, aggregate_attr| + if mapping.size == 1 && !object.respond_to?(aggregate_attr) + build(table.arel_attribute(field_attr), object) + else + build(table.arel_attribute(field_attr), object.send(aggregate_attr)) + end + end.reduce(&:and) + end + queries.reduce(&:or) else build(table.arel_attribute(key), value) end @@ -97,6 +107,7 @@ module ActiveRecord end private + attr_reader :table def associated_predicate_builder(association_name) self.class.new(table.associated_table(association_name)) 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 2fd75c8958..64bf83e3c1 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -34,8 +34,7 @@ module ActiveRecord array_predicates.inject(&:or) end - protected - + private attr_reader :predicate_builder module NullPredicate # :nodoc: diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 28c7483c95..88cd71cf69 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -12,12 +12,9 @@ module ActiveRecord [associated_table.association_join_foreign_key.to_s => ids] end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :value - private def ids case value when Relation diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb index 112821135f..10c5c1a66a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -11,8 +11,7 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb index 34db266f05..e8c9f60860 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -12,8 +12,7 @@ module ActiveRecord attribute.eq(bind) end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index e8e2f2c626..aae04d9348 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -17,27 +17,26 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :values - private def type_to_ids_mapping default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + values.each_with_object(default_hash) do |value, hash| + hash[klass(value).polymorphic_name] << convert_to_id(value) + end end def primary_key(value) - associated_table.association_join_primary_key(base_class(value)) + associated_table.association_join_primary_key(klass(value)) end - def base_class(value) + def klass(value) case value when Base - value.class.base_class + value.class when Relation - value.klass.base_class + value.klass end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 6d16579708..44bb2c7ab6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -16,15 +16,16 @@ module ActiveRecord def call(attribute, value) begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin) end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end) - if value.begin.respond_to?(:infinite?) && value.begin.infinite? - if value.end.respond_to?(:infinite?) && value.end.infinite? + + if begin_bind.value.infinity? + if end_bind.value.infinity? attribute.not_in([]) elsif value.exclude_end? attribute.lt(end_bind) else attribute.lteq(end_bind) end - elsif value.end.respond_to?(:infinite?) && value.end.infinite? + elsif end_bind.value.infinity? attribute.gteq(begin_bind) elsif value.exclude_end? attribute.gteq(begin_bind).and(attribute.lt(end_bind)) @@ -33,8 +34,7 @@ module ActiveRecord end end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index 3532f28858..f64bd30d38 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -21,6 +21,23 @@ module ActiveRecord !value_before_type_cast.is_a?(StatementCache::Substitute) && (value_before_type_cast.nil? || value_for_database.nil?) end + + def boundable? + return @_boundable if defined?(@_boundable) + nil? + @_boundable = true + rescue ::RangeError + @_boundable = false + end + + def infinity? + _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + end + + private + def _infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + 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 009624e194..52405f21a1 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -231,6 +231,7 @@ module ActiveRecord end def _select!(*fields) # :nodoc: + fields.reject!(&:blank?) fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field @@ -327,8 +328,8 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :includes, :from, - :readonly, :having]) + :limit, :offset, :joins, :left_outer_joins, + :includes, :from, :readonly, :having]) # Removes an unwanted relation that is already defined on a chain of relations. # This is useful when passing around chains of relations and would like to @@ -375,6 +376,7 @@ module ActiveRecord args.each do |scope| case scope when Symbol + scope = :left_outer_joins if scope == :left_joins if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end @@ -892,8 +894,13 @@ module ActiveRecord self end - def skip_query_cache! # :nodoc: - self.skip_query_cache_value = true + def skip_query_cache!(value = true) # :nodoc: + self.skip_query_cache_value = value + self + end + + def skip_preloading! # :nodoc: + self.skip_preloading_value = true self end @@ -902,11 +909,12 @@ module ActiveRecord @arel ||= build_arel(aliases) end + # Returns a relation value with a given name + def get_value(name) # :nodoc: + @values.fetch(name, DEFAULT_VALUES[name]) + end + protected - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values.fetch(name, DEFAULT_VALUES[name]) - end # Sets the relation value with the given name def set_value(name, value) # :nodoc: @@ -978,6 +986,8 @@ module ActiveRecord case join when Hash, Symbol, Array :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join else raise ArgumentError, "only Hash, Symbol and Array are allowed" end @@ -1008,19 +1018,17 @@ module ActiveRecord def build_join_query(manager, buckets, join_type, aliases) buckets.default = [] - association_joins = buckets[:association_join] - stashed_association_joins = buckets[:stashed_join] - join_nodes = buckets[:join_node].uniq - string_joins = buckets[:string_join].map(&:strip).uniq + association_joins = buckets[:association_join] + stashed_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(string_joins) alias_tracker = alias_tracker(join_list, aliases) - join_dependency = ActiveRecord::Associations::JoinDependency.new( - klass, table, association_joins, alias_tracker - ) + join_dependency = construct_join_dependency(association_joins) - joins = join_dependency.join_constraints(stashed_association_joins, join_type) + joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) joins.each { |join| manager.from(join) } manager.join_sources.concat(join_list) @@ -1028,7 +1036,7 @@ module ActiveRecord alias_tracker.aliases end - def convert_join_strings_to_ast(table, joins) + def convert_join_strings_to_ast(joins) joins .flatten .reject(&:blank?) @@ -1046,11 +1054,13 @@ module ActiveRecord end def arel_columns(columns) - columns.map do |field| + columns.flat_map do |field| if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value arel_attribute(field) elsif Symbol === field connection.quote_table_name(field.to_s) + elsif Proc === field + field.call else field end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 617d8de8b2..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -69,7 +69,7 @@ module ActiveRecord private def relation_with(values) - result = Relation.create(klass, table, predicate_builder, values) + result = Relation.create(klass, values: values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 4ae94f4bfe..c1b3eea9df 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -14,7 +14,6 @@ module ActiveRecord parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))] when Hash attributes = predicate_builder.resolve_column_aliases(opts) - attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! parts = predicate_builder.build_from_hash(attributes) @@ -27,8 +26,7 @@ module ActiveRecord WhereClause.new(parts) end - protected - + private attr_reader :klass, :predicate_builder end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index e54e8086dd..7f1c2fd7eb 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -43,6 +43,11 @@ module ActiveRecord @column_types = column_types end + # Returns true if this result set includes the column named +name+ + def includes_column?(name) + @columns.include? name + end + # Returns the number of elements in the rows array. def length @rows.length @@ -97,12 +102,21 @@ module ActiveRecord 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.deserialize(value) } - end + if columns.one? + # Separated to avoid allocating an array per row - columns.one? ? result.map!(&:first) : result + type = column_type(columns.first, type_overrides) + + rows.map do |(value)| + type.deserialize(value) + end + else + types = columns.map { |name| column_type(name, type_overrides) } + + rows.map do |values| + Array.new(values.size) { |i| types[i].deserialize(values[i]) } + end + end end def initialize_copy(other) @@ -125,7 +139,7 @@ module ActiveRecord begin # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash - columns = @columns.map { |c| c.dup.freeze } + columns = @columns.map(&:-@) @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 58da106092..c6c268855e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -84,7 +84,7 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - type = type_for_attribute(attr.to_s) + type = type_for_attribute(attr) value = type.serialize(type.cast(value)) "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(", ") @@ -155,10 +155,12 @@ module ActiveRecord if aggregation = reflect_on_aggregation(attr.to_sym) mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value + expanded_attrs[field_attr] = if value.is_a?(Array) + value.map { |it| it.send(aggregate_attr) } + elsif mapping.size == 1 && !value.respond_to?(aggregate_attr) + value else - expanded_attrs[field_attr] = value.send(aggregate_attr) + value.send(aggregate_attr) end end else @@ -167,6 +169,7 @@ module ActiveRecord end expanded_attrs end + deprecate :expand_hash_conditions_for_aggregates def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 1e121f2a09..216359867c 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -55,7 +55,7 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 16ccba6b6c..d475e77444 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,6 +17,12 @@ module ActiveRecord # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables, default: [] + ## + # :singleton-method: + # Specify a custom regular expression matching foreign keys which name + # should not be dumped to db/schema.rb. + cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/ + class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) connection.create_schema_dumper(generate_options(config)).dump(stream) @@ -44,7 +50,7 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @version = Migrator::current_version rescue nil + @version = connection.migration_context.current_version rescue nil @options = options end @@ -65,11 +71,11 @@ module ActiveRecord # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. @@ -210,7 +216,7 @@ HEADER parts << "primary_key: #{foreign_key.primary_key.inspect}" end - if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + if foreign_key.export_name_on_schema_dump? parts << "name: #{foreign_key.name.inspect}" end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 752655aa05..a784001587 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -183,7 +183,7 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| scope = all - scope = scope.instance_exec(*args, &body) || scope + scope = scope._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 59acd63a0f..b41d3504fd 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -114,8 +114,7 @@ module ActiveRecord end end - protected - + private attr_reader :query_builder, :bind_map, :klass end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 202b82fa61..3537e2d008 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -17,8 +17,8 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for - # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+ + # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. @@ -31,10 +31,18 @@ module ActiveRecord # # class User < ActiveRecord::Base # store :settings, accessors: [ :color, :homepage ], coder: JSON + # store :parent, accessors: [ :name ], coder: JSON, prefix: true + # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner + # store :settings, accessors: [ :two_factor_auth ], suffix: true + # store :settings, accessors: [ :login_retry ], suffix: :config # end # - # u = User.new(color: 'black', homepage: '37signals.com') + # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily') # u.color # Accessor stored attribute + # u.parent_name # Accessor stored attribute with prefix + # u.partner_name # Accessor stored attribute with custom prefix + # u.two_factor_auth_settings # Accessor stored attribute with suffix + # u.login_retry_config # Accessor stored attribute with custom suffix # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # # # There is no difference between strings and symbols for accessing custom attributes @@ -44,11 +52,13 @@ module ActiveRecord # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants + # store_accessor :parent, :birthday, prefix: true + # store_accessor :settings, :secret_question, suffix: :config # end # # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # - # User.stored_attributes[:settings] # [:color, :homepage] + # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry] # # == Overwriting default accessors # @@ -81,19 +91,40 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) - store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors end - def store_accessor(store_attribute, *keys) + def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) keys = keys.flatten + accessor_prefix = + case prefix + when String, Symbol + "#{prefix}_" + when TrueClass + "#{store_attribute}_" + else + "" + end + accessor_suffix = + case suffix + when String, Symbol + "_#{suffix}" + when TrueClass + "_#{store_attribute}" + else + "" + end + _store_accessors_module.module_eval do keys.each do |key| - define_method("#{key}=") do |value| + accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}" + + define_method("#{accessor_key}=") do |value| write_store_attribute(store_attribute, key, value) end - define_method(key) do + define_method(accessor_key) do read_store_attribute(store_attribute, key) end end @@ -135,7 +166,7 @@ module ActiveRecord end def store_accessor_for(store_attribute) - type_for_attribute(store_attribute.to_s).accessor + type_for_attribute(store_attribute).accessor end class HashAccessor # :nodoc: diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 0459cbdc59..b67479fb6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -30,7 +30,7 @@ module ActiveRecord def type(column_name) if klass - klass.type_for_attribute(column_name.to_s) + klass.type_for_attribute(column_name) else Type.default_value end @@ -65,10 +65,15 @@ module ActiveRecord association && association.polymorphic? end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + def aggregated_with?(aggregation_name) + klass && reflect_on_aggregation(aggregation_name) + end + + def reflect_on_aggregation(aggregation_name) + klass.reflect_on_aggregation(aggregation_name) + end + private attr_reader :klass, :arel_table, :association end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 4657e51e6d..fd36c0abd2 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -8,7 +8,7 @@ module ActiveRecord # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used with Rake tasks provided by Active Record. + # The tasks defined here are used with Rails commands 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 @@ -54,10 +54,10 @@ module ActiveRecord def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] - current = ActiveRecord::Migrator.current_environment - stored = ActiveRecord::Migrator.last_stored_environment + current = ActiveRecord::Base.connection.migration_context.current_environment + stored = ActiveRecord::Base.connection.migration_context.last_stored_environment - if ActiveRecord::Migrator.protected_environment? + if ActiveRecord::Base.connection.migration_context.protected_environment? raise ActiveRecord::ProtectedEnvironmentError.new(stored) end @@ -117,9 +117,9 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).create - $stdout.puts "Created database '#{configuration['database']}'" + $stdout.puts "Created database '#{configuration['database']}'" if verbose? rescue DatabaseAlreadyExists - $stderr.puts "Database '#{configuration['database']}' already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -134,6 +134,18 @@ module ActiveRecord end end + def for_each + databases = Rails.application.config.load_database_yaml + database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + + # if this is a single database application we don't want tasks for each primary database + return if database_configs.count == 1 + + database_configs.each do |db_config| + yield db_config.spec_name + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration @@ -144,7 +156,7 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).drop - $stdout.puts "Dropped database '#{configuration['database']}'" + $stdout.puts "Dropped database '#{configuration['database']}'" if verbose? rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error @@ -166,10 +178,9 @@ module ActiveRecord def migrate check_target_version - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true scope = ENV["SCOPE"] - verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, target_version) do |migration| + verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! @@ -234,9 +245,10 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end - def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc: - file ||= schema_file(format) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc: + file ||= dump_filename(spec_name, format) + verbose_was, Migration.verbose = Migration.verbose, verbose? && ENV["VERBOSE"] check_schema_file(file) ActiveRecord::Base.establish_connection(configuration) @@ -250,20 +262,36 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table ActiveRecord::InternalMetadata[:environment] = environment + ensure + Migration.verbose = verbose_was end def schema_file(format = ActiveRecord::Base.schema_format) + File.join(db_dir, schema_file_type(format)) + end + + def schema_file_type(format = ActiveRecord::Base.schema_format) case format when :ruby - File.join(db_dir, "schema.rb") + "schema.rb" when :sql - File.join(db_dir, "structure.sql") + "structure.sql" end end + def dump_filename(namespace, format = ActiveRecord::Base.schema_format) + filename = if namespace == "primary" + schema_file_type(format) + else + "#{namespace}_#{schema_file_type(format)}" + end + + ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration, configuration_environment| - load_schema configuration, format, file, configuration_environment + each_current_configuration(environment) { |configuration, spec_name, env| + load_schema(configuration, format, file, env, spec_name) } ActiveRecord::Base.establish_connection(environment.to_sym) end @@ -297,6 +325,9 @@ module ActiveRecord end private + def verbose? + ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + end def class_for_adapter(adapter) _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } @@ -310,10 +341,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| - next unless configuration["database"] - - yield configuration, configuration_environment + environments.each do |env| + ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| + yield configuration, spec_name, env + end end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index e697fa6def..eddc6fa223 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -68,9 +68,7 @@ module ActiveRecord private - def configuration - @configuration - end + attr_reader :configuration def configuration_without_database configuration.merge("database" => nil) diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 647e066137..533e6953a4 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -90,9 +90,7 @@ module ActiveRecord private - def configuration - @configuration - end + attr_reader :configuration def encoding configuration["encoding"] || DEFAULT_ENCODING diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index dfe599c4dd..d0bad05176 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -60,13 +60,7 @@ module ActiveRecord private - def configuration - @configuration - end - - def root - @root - end + attr_reader :configuration, :root def run_cmd(cmd, args, out) fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb new file mode 100644 index 0000000000..5b4efe22c9 --- /dev/null +++ b/activerecord/lib/active_record/test_databases.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "active_support/testing/parallelization" + +module ActiveRecord + module TestDatabases # :nodoc: + ActiveSupport::Testing::Parallelization.after_fork_hook do |i| + create_and_load_schema(i, spec_name: Rails.env) + end + + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| + drop(spec_name: Rails.env) + end + + def self.create_and_load_schema(i, spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + + connection_spec = ActiveRecord::Base.configurations[spec_name] + + connection_spec["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) + ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ensure + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ENV["VERBOSE"] = old + end + + def self.drop(spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + connection_spec = ActiveRecord::Base.configurations[spec_name] + + ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ensure + ENV["VERBOSE"] = old + end + end +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5da3759e5a..d32f971ad1 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -52,7 +52,13 @@ module ActiveRecord clear_timestamp_attributes end - class_methods do + module ClassMethods # :nodoc: + def touch_attributes_with_time(*names, time: nil) + attribute_names = timestamp_attributes_for_update_in_model + attribute_names |= names.map(&:to_s) + attribute_names.index_with(time ||= current_time_from_proper_timezone) + end + private def timestamp_attributes_for_create_in_model timestamp_attributes_for_create.select { |c| column_names.include?(c) } diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 97cba5d1c7..c5d5fca672 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -306,9 +306,7 @@ module ActiveRecord end def save(*) #:nodoc: - rollback_active_record_state! do - with_transaction_returning_status { super } - end + with_transaction_returning_status { super } end def save!(*) #:nodoc: @@ -319,17 +317,6 @@ module ActiveRecord with_transaction_returning_status { super } end - # Reset id and @new_record if the transaction rolls back. - def rollback_active_record_state! - remember_transaction_record_state - yield - rescue Exception - restore_transaction_record_state - raise - ensure - clear_transaction_record_state - end - def before_committed! # :nodoc: _run_before_commit_without_transaction_enrollment_callbacks _run_before_commit_callbacks @@ -340,11 +327,13 @@ module ActiveRecord # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: - if should_run_callbacks && destroyed? || persisted? + if should_run_callbacks && (destroyed? || persisted?) + @_committed_already_called = true _run_commit_without_transaction_enrollment_callbacks _run_commit_callbacks end ensure + @_committed_already_called = false force_clear_transaction_record_state end @@ -382,13 +371,7 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - begin - status = yield - rescue ActiveRecord::Rollback - clear_transaction_record_state - status = nil - end - + status = yield raise ActiveRecord::Rollback unless status end status @@ -399,16 +382,26 @@ module ActiveRecord end private + attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback # 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 - @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( + id: id, new_record: @new_record, destroyed: @destroyed, frozen?: frozen?, ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + remember_new_record_before_last_commit + end + + def remember_new_record_before_last_commit + if _committed_already_called + @_new_record_before_last_commit = false + else + @_new_record_before_last_commit = @_start_transaction_state[:new_record] + end end # Clear the new record state and id of a record. @@ -440,22 +433,16 @@ module ActiveRecord end end - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) - @_start_transaction_state[state] - end - # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. def transaction_include_any_action?(actions) actions.any? do |action| case action when :create - transaction_record_state(:new_record) - when :destroy - defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback + persisted? && @_new_record_before_last_commit when :update - !(transaction_record_state(:new_record) || destroyed?) && - (defined?(@_trigger_update_callback) && @_trigger_update_callback) + !(@_new_record_before_last_commit || destroyed?) && _trigger_update_callback + when :destroy + _trigger_destroy_callback end end end @@ -491,7 +478,8 @@ module ActiveRecord def update_attributes_from_transaction_state(transaction_state) if transaction_state && transaction_state.finalized? - restore_transaction_record_state if transaction_state.rolledback? + restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback? + force_clear_transaction_record_state if transaction_state.fully_committed? clear_transaction_record_state if transaction_state.fully_completed? end end diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb index 3cf70eafb8..82661a328a 100644 --- a/activerecord/lib/active_record/translation.rb +++ b/activerecord/lib/active_record/translation.rb @@ -10,7 +10,7 @@ module ActiveRecord classes = [klass] return classes if klass == ActiveRecord::Base - while klass != klass.base_class + while !klass.base_class? classes << klass = klass.superclass end classes diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index e7468aa542..b300fdfa05 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -52,8 +52,6 @@ module ActiveRecord priority <=> other.priority end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block, :adapter, :override @@ -114,13 +112,8 @@ module ActiveRecord super | 4 end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :options, :klass - private + attr_reader :options, :klass def matches_options?(**kwargs) options.all? do |key, value| diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index e882784691..0a2f6cb9fb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -51,6 +51,10 @@ module ActiveRecord end end + def force_equality?(value) + coder.respond_to?(:object_class) && value.is_a?(coder.object_class) + end + private def default_value?(value) diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index af4e4e37e2..7cf8181d8e 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -14,15 +14,10 @@ module ActiveRecord connection.type_cast_from_column(column, value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :table_name delegate :connection, to: :@klass - private - def column_for(attribute_name) if connection.schema_cache.data_source_exists?(table_name) connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index d51350ba83..663cdadb03 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -9,14 +9,11 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - type = types.type_for_attribute(attr_name.to_s) + type = types.type_for_attribute(attr_name) type.serialize(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :types end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4c2c5dd852..5a1dbc8e53 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -23,7 +23,7 @@ module ActiveRecord relation = build_relation(finder_class, attribute, value) if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) + relation = relation.where.not(finder_class.primary_key => record.id_in_database) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb new file mode 100644 index 0000000000..7d04e1cac6 --- /dev/null +++ b/activerecord/lib/arel.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "arel/errors" + +require "arel/crud" +require "arel/factory_methods" + +require "arel/expressions" +require "arel/predications" +require "arel/window_predications" +require "arel/math" +require "arel/alias_predication" +require "arel/order_predications" +require "arel/table" +require "arel/attributes" +require "arel/compatibility/wheres" + +require "arel/visitors" +require "arel/collectors/sql_string" + +require "arel/tree_manager" +require "arel/insert_manager" +require "arel/select_manager" +require "arel/update_manager" +require "arel/delete_manager" +require "arel/nodes" + +module Arel # :nodoc: all + VERSION = "10.0.0" + + def self.sql(raw_sql) + Arel::Nodes::SqlLiteral.new raw_sql + end + + def self.star + sql "*" + end + ## Convenience Alias + Node = Arel::Nodes::Node +end diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb new file mode 100644 index 0000000000..4abbbb7ef6 --- /dev/null +++ b/activerecord/lib/arel/alias_predication.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module AliasPredication + def as(other) + Nodes::As.new self, Nodes::SqlLiteral.new(other) + end + end +end diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb new file mode 100644 index 0000000000..35d586c948 --- /dev/null +++ b/activerecord/lib/arel/attributes.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "arel/attributes/attribute" + +module Arel # :nodoc: all + module Attributes + ### + # Factory method to wrap a raw database +column+ to an Arel Attribute. + def self.for(column) + case column.type + when :string, :text, :binary then String + when :integer then Integer + when :float then Float + when :decimal then Decimal + when :date, :datetime, :timestamp, :time then Time + when :boolean then Boolean + else + Undefined + end + end + end +end diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb new file mode 100644 index 0000000000..ecf499a23e --- /dev/null +++ b/activerecord/lib/arel/attributes/attribute.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Attributes + class Attribute < Struct.new :relation, :name + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + include Arel::Math + + ### + # Create a node for lowering this attribute + def lower + relation.lower self + end + + def type_cast_for_database(value) + relation.type_cast_for_database(name, value) + end + + def able_to_type_cast? + relation.able_to_type_cast? + end + end + + class String < Attribute; end + class Time < Attribute; end + class Boolean < Attribute; end + class Decimal < Attribute; end + class Float < Attribute; end + class Integer < Attribute; end + class Undefined < Attribute; end + end + + Attribute = Attributes::Attribute +end diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb new file mode 100644 index 0000000000..6f8912575d --- /dev/null +++ b/activerecord/lib/arel/collectors/bind.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class Bind + def initialize + @binds = [] + end + + def <<(str) + self + end + + def add_bind(bind) + @binds << bind + self + end + + def value + @binds + end + end + end +end diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb new file mode 100644 index 0000000000..d040d8598d --- /dev/null +++ b/activerecord/lib/arel/collectors/composite.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class Composite + def initialize(left, right) + @left = left + @right = right + end + + def <<(str) + left << str + right << str + self + end + + def add_bind(bind, &block) + left.add_bind bind, &block + right.add_bind bind, &block + self + end + + def value + [left.value, right.value] + end + + protected + + attr_reader :left, :right + end + end +end diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb new file mode 100644 index 0000000000..687d7fbf2f --- /dev/null +++ b/activerecord/lib/arel/collectors/plain_string.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class PlainString + def initialize + @str = "".dup + end + + def value + @str + end + + def <<(str) + @str << str + self + end + end + end +end diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb new file mode 100644 index 0000000000..c293a89a74 --- /dev/null +++ b/activerecord/lib/arel/collectors/sql_string.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "arel/collectors/plain_string" + +module Arel # :nodoc: all + module Collectors + class SQLString < PlainString + def initialize(*) + super + @bind_index = 1 + end + + def add_bind(bind) + self << yield(@bind_index) + @bind_index += 1 + self + end + + def compile(bvs) + value + end + end + end +end diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb new file mode 100644 index 0000000000..3f40eec8a8 --- /dev/null +++ b/activerecord/lib/arel/collectors/substitute_binds.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class SubstituteBinds + def initialize(quoter, delegate_collector) + @quoter = quoter + @delegate = delegate_collector + end + + def <<(str) + delegate << str + self + end + + def add_bind(bind) + self << quoter.quote(bind) + end + + def value + delegate.value + end + + protected + + attr_reader :quoter, :delegate + end + end +end diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb new file mode 100644 index 0000000000..c8a73f0dae --- /dev/null +++ b/activerecord/lib/arel/compatibility/wheres.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Compatibility # :nodoc: + class Wheres # :nodoc: + include Enumerable + + module Value # :nodoc: + attr_accessor :visitor + def value + visitor.accept self + end + + def name + super.to_sym + end + end + + def initialize(engine, collection) + @engine = engine + @collection = collection + end + + def each + to_sql = Visitors::ToSql.new @engine + + @collection.each { |c| + c.extend(Value) + c.visitor = to_sql + yield c + } + end + end + end +end diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb new file mode 100644 index 0000000000..e8a563ca4a --- /dev/null +++ b/activerecord/lib/arel/crud.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + ### + # FIXME hopefully we can remove this + module Crud + def compile_update(values, pk) + um = UpdateManager.new + + if Nodes::SqlLiteral === values + relation = @ctx.from + else + relation = values.first.first.relation + end + um.key = pk + um.table relation + um.set values + um.take @ast.limit.expr if @ast.limit + um.order(*@ast.orders) + um.wheres = @ctx.wheres + um + end + + def compile_insert(values) + im = create_insert + im.insert values + im + end + + def create_insert + InsertManager.new + end + + def compile_delete + dm = DeleteManager.new + dm.take @ast.limit.expr if @ast.limit + dm.wheres = @ctx.wheres + dm.from @ctx.froms + dm + end + end +end diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb new file mode 100644 index 0000000000..2def581009 --- /dev/null +++ b/activerecord/lib/arel/delete_manager.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class DeleteManager < Arel::TreeManager + def initialize + super + @ast = Nodes::DeleteStatement.new + @ctx = @ast + end + + def from(relation) + @ast.relation = relation + self + end + + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def wheres=(list) + @ast.wheres = list + end + end +end diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb new file mode 100644 index 0000000000..2f8d5e3c02 --- /dev/null +++ b/activerecord/lib/arel/errors.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class ArelError < StandardError + end + + class EmptyJoinError < ArelError + end +end diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb new file mode 100644 index 0000000000..da8afb338c --- /dev/null +++ b/activerecord/lib/arel/expressions.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Expressions + def count(distinct = false) + Nodes::Count.new [self], distinct + end + + def sum + Nodes::Sum.new [self] + end + + def maximum + Nodes::Max.new [self] + end + + def minimum + Nodes::Min.new [self] + end + + def average + Nodes::Avg.new [self] + end + + def extract(field) + Nodes::Extract.new [self], field + end + end +end diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb new file mode 100644 index 0000000000..b828bc274e --- /dev/null +++ b/activerecord/lib/arel/factory_methods.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + ### + # Methods for creating various nodes + module FactoryMethods + def create_true + Arel::Nodes::True.new + end + + def create_false + Arel::Nodes::False.new + end + + def create_table_alias(relation, name) + Nodes::TableAlias.new(relation, name) + end + + def create_join(to, constraint = nil, klass = Nodes::InnerJoin) + klass.new(to, constraint) + end + + def create_string_join(to) + create_join to, nil, Nodes::StringJoin + end + + def create_and(clauses) + Nodes::And.new clauses + end + + def create_on(expr) + Nodes::On.new expr + end + + def grouping(expr) + Nodes::Grouping.new expr + end + + ### + # Create a LOWER() function + def lower(column) + Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)] + end + end +end diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb new file mode 100644 index 0000000000..c90fc33a48 --- /dev/null +++ b/activerecord/lib/arel/insert_manager.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class InsertManager < Arel::TreeManager + def initialize + super + @ast = Nodes::InsertStatement.new + end + + def into(table) + @ast.relation = table + self + end + + def columns; @ast.columns end + def values=(val); @ast.values = val; end + + def select(select) + @ast.select = select + end + + def insert(fields) + return if fields.empty? + + if String === fields + @ast.values = Nodes::SqlLiteral.new(fields) + else + @ast.relation ||= fields.first.first.relation + + values = [] + + fields.each do |column, value| + @ast.columns << column + values << value + end + @ast.values = create_values values, @ast.columns + end + self + end + + def create_values(values, columns) + Nodes::Values.new values, columns + end + + def create_values_list(rows) + Nodes::ValuesList.new(rows) + end + end +end diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb new file mode 100644 index 0000000000..2359f13148 --- /dev/null +++ b/activerecord/lib/arel/math.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Math + def *(other) + Arel::Nodes::Multiplication.new(self, other) + end + + def +(other) + Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other)) + end + + def -(other) + Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other)) + end + + def /(other) + Arel::Nodes::Division.new(self, other) + end + + def &(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other)) + end + + def |(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other)) + end + + def ^(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other)) + end + + def <<(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other)) + end + + def >>(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other)) + end + + def ~@ + Arel::Nodes::BitwiseNot.new(self) + end + end +end diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb new file mode 100644 index 0000000000..5af0e532e2 --- /dev/null +++ b/activerecord/lib/arel/nodes.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# node +require "arel/nodes/node" +require "arel/nodes/node_expression" +require "arel/nodes/select_statement" +require "arel/nodes/select_core" +require "arel/nodes/insert_statement" +require "arel/nodes/update_statement" +require "arel/nodes/bind_param" + +# terminal + +require "arel/nodes/terminal" +require "arel/nodes/true" +require "arel/nodes/false" + +# unary +require "arel/nodes/unary" +require "arel/nodes/grouping" +require "arel/nodes/ascending" +require "arel/nodes/descending" +require "arel/nodes/unqualified_column" +require "arel/nodes/with" + +# binary +require "arel/nodes/binary" +require "arel/nodes/equality" +require "arel/nodes/in" # Why is this subclassed from equality? +require "arel/nodes/join_source" +require "arel/nodes/delete_statement" +require "arel/nodes/table_alias" +require "arel/nodes/infix_operation" +require "arel/nodes/unary_operation" +require "arel/nodes/over" +require "arel/nodes/matches" +require "arel/nodes/regexp" + +# nary +require "arel/nodes/and" + +# function +# FIXME: Function + Alias can be rewritten as a Function and Alias node. +# We should make Function a Unary node and deprecate the use of "aliaz" +require "arel/nodes/function" +require "arel/nodes/count" +require "arel/nodes/extract" +require "arel/nodes/values" +require "arel/nodes/values_list" +require "arel/nodes/named_function" + +# windows +require "arel/nodes/window" + +# conditional expressions +require "arel/nodes/case" + +# joins +require "arel/nodes/full_outer_join" +require "arel/nodes/inner_join" +require "arel/nodes/outer_join" +require "arel/nodes/right_outer_join" +require "arel/nodes/string_join" + +require "arel/nodes/sql_literal" + +require "arel/nodes/casted" diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb new file mode 100644 index 0000000000..c530a77bfb --- /dev/null +++ b/activerecord/lib/arel/nodes/and.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class And < Arel::Nodes::Node + attr_reader :children + + def initialize(children) + super() + @children = children + end + + def left + children.first + end + + def right + children[1] + end + + def hash + children.hash + end + + def eql?(other) + self.class == other.class && + self.children == other.children + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb new file mode 100644 index 0000000000..8b617f4df5 --- /dev/null +++ b/activerecord/lib/arel/nodes/ascending.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Ascending < Ordering + def reverse + Descending.new(expr) + end + + def direction + :asc + end + + def ascending? + true + end + + def descending? + false + end + end + end +end diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb new file mode 100644 index 0000000000..e184e99c73 --- /dev/null +++ b/activerecord/lib/arel/nodes/binary.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Binary < Arel::Nodes::NodeExpression + attr_accessor :left, :right + + def initialize(left, right) + super() + @left = left + @right = right + end + + def initialize_copy(other) + super + @left = @left.clone if @left + @right = @right.clone if @right + end + + def hash + [self.class, @left, @right].hash + end + + def eql?(other) + self.class == other.class && + self.left == other.left && + self.right == other.right + end + alias :== :eql? + end + + %w{ + As + Assignment + Between + GreaterThan + GreaterThanOrEqual + Join + LessThan + LessThanOrEqual + NotEqual + NotIn + Or + Union + UnionAll + Intersect + Except + }.each do |name| + const_set name, Class.new(Binary) + end + end +end diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb new file mode 100644 index 0000000000..53c5563d93 --- /dev/null +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class BindParam < Node + attr_accessor :value + + def initialize(value) + @value = value + super() + end + + def hash + [self.class, self.value].hash + end + + def eql?(other) + other.is_a?(BindParam) && + value == other.value + end + alias :== :eql? + + def nil? + value.nil? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb new file mode 100644 index 0000000000..654a54825e --- /dev/null +++ b/activerecord/lib/arel/nodes/case.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Case < Arel::Nodes::Node + attr_accessor :case, :conditions, :default + + def initialize(expression = nil, default = nil) + @case = expression + @conditions = [] + @default = default + end + + def when(condition, expression = nil) + @conditions << When.new(Nodes.build_quoted(condition), expression) + self + end + + def then(expression) + @conditions.last.right = Nodes.build_quoted(expression) + self + end + + def else(expression) + @default = Else.new Nodes.build_quoted(expression) + self + end + + def initialize_copy(other) + super + @case = @case.clone if @case + @conditions = @conditions.map { |x| x.clone } + @default = @default.clone if @default + end + + def hash + [@case, @conditions, @default].hash + end + + def eql?(other) + self.class == other.class && + self.case == other.case && + self.conditions == other.conditions && + self.default == other.default + end + alias :== :eql? + end + + class When < Binary # :nodoc: + end + + class Else < Unary # :nodoc: + end + end +end diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb new file mode 100644 index 0000000000..c1e6e97d6d --- /dev/null +++ b/activerecord/lib/arel/nodes/casted.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Casted < Arel::Nodes::NodeExpression # :nodoc: + attr_reader :val, :attribute + def initialize(val, attribute) + @val = val + @attribute = attribute + super() + end + + def nil?; @val.nil?; end + + def hash + [self.class, val, attribute].hash + end + + def eql?(other) + self.class == other.class && + self.val == other.val && + self.attribute == other.attribute + end + alias :== :eql? + end + + class Quoted < Arel::Nodes::Unary # :nodoc: + alias :val :value + def nil?; val.nil?; end + end + + def self.build_quoted(other, attribute = nil) + case other + when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral + other + else + case attribute + when Arel::Attributes::Attribute + Casted.new other, attribute + else + Quoted.new other + end + end + end + end +end diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb new file mode 100644 index 0000000000..880464639d --- /dev/null +++ b/activerecord/lib/arel/nodes/count.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Count < Arel::Nodes::Function + def initialize(expr, distinct = false, aliaz = nil) + super(expr, aliaz) + @distinct = distinct + end + end + end +end diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb new file mode 100644 index 0000000000..eaac05e2f6 --- /dev/null +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class DeleteStatement < Arel::Nodes::Node + attr_accessor :left, :right + attr_accessor :limit + + alias :relation :left + alias :relation= :left= + alias :wheres :right + alias :wheres= :right= + + def initialize(relation = nil, wheres = []) + super() + @left = relation + @right = wheres + end + + def initialize_copy(other) + super + @left = @left.clone if @left + @right = @right.clone if @right + end + + def hash + [self.class, @left, @right].hash + end + + def eql?(other) + self.class == other.class && + self.left == other.left && + self.right == other.right + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb new file mode 100644 index 0000000000..f3f6992ca8 --- /dev/null +++ b/activerecord/lib/arel/nodes/descending.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Descending < Ordering + def reverse + Ascending.new(expr) + end + + def direction + :desc + end + + def ascending? + false + end + + def descending? + true + end + end + end +end diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb new file mode 100644 index 0000000000..2aa85a977e --- /dev/null +++ b/activerecord/lib/arel/nodes/equality.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Equality < Arel::Nodes::Binary + def operator; :== end + alias :operand1 :left + alias :operand2 :right + end + end +end diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb new file mode 100644 index 0000000000..5799ee9b8f --- /dev/null +++ b/activerecord/lib/arel/nodes/extract.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Extract < Arel::Nodes::Unary + attr_accessor :field + + def initialize(expr, field) + super(expr) + @field = field + end + + def hash + super ^ @field.hash + end + + def eql?(other) + super && + self.field == other.field + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb new file mode 100644 index 0000000000..1e5bf04be5 --- /dev/null +++ b/activerecord/lib/arel/nodes/false.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class False < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb new file mode 100644 index 0000000000..91bb81f2e3 --- /dev/null +++ b/activerecord/lib/arel/nodes/full_outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class FullOuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb new file mode 100644 index 0000000000..0a439b39f5 --- /dev/null +++ b/activerecord/lib/arel/nodes/function.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Function < Arel::Nodes::NodeExpression + include Arel::WindowPredications + attr_accessor :expressions, :alias, :distinct + + def initialize(expr, aliaz = nil) + super() + @expressions = expr + @alias = aliaz && SqlLiteral.new(aliaz) + @distinct = false + end + + def as(aliaz) + self.alias = SqlLiteral.new(aliaz) + self + end + + def hash + [@expressions, @alias, @distinct].hash + end + + def eql?(other) + self.class == other.class && + self.expressions == other.expressions && + self.alias == other.alias && + self.distinct == other.distinct + end + alias :== :eql? + end + + %w{ + Sum + Exists + Max + Min + Avg + }.each do |name| + const_set(name, Class.new(Function)) + end + end +end diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb new file mode 100644 index 0000000000..4d0bd69d4d --- /dev/null +++ b/activerecord/lib/arel/nodes/grouping.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Grouping < Unary + end + end +end diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb new file mode 100644 index 0000000000..2be45d6f99 --- /dev/null +++ b/activerecord/lib/arel/nodes/in.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class In < Equality + end + end +end diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb new file mode 100644 index 0000000000..bc7e20dcc6 --- /dev/null +++ b/activerecord/lib/arel/nodes/infix_operation.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InfixOperation < Binary + include Arel::Expressions + include Arel::Predications + include Arel::OrderPredications + include Arel::AliasPredication + include Arel::Math + + attr_reader :operator + + def initialize(operator, left, right) + super(left, right) + @operator = operator + end + end + + class Multiplication < InfixOperation + def initialize(left, right) + super(:*, left, right) + end + end + + class Division < InfixOperation + def initialize(left, right) + super(:/, left, right) + end + end + + class Addition < InfixOperation + def initialize(left, right) + super(:+, left, right) + end + end + + class Subtraction < InfixOperation + def initialize(left, right) + super(:-, left, right) + end + end + + class Concat < InfixOperation + def initialize(left, right) + super("||", left, right) + end + end + + class BitwiseAnd < InfixOperation + def initialize(left, right) + super(:&, left, right) + end + end + + class BitwiseOr < InfixOperation + def initialize(left, right) + super(:|, left, right) + end + end + + class BitwiseXor < InfixOperation + def initialize(left, right) + super(:^, left, right) + end + end + + class BitwiseShiftLeft < InfixOperation + def initialize(left, right) + super(:<<, left, right) + end + end + + class BitwiseShiftRight < InfixOperation + def initialize(left, right) + super(:>>, left, right) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb new file mode 100644 index 0000000000..519fafad09 --- /dev/null +++ b/activerecord/lib/arel/nodes/inner_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InnerJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb new file mode 100644 index 0000000000..d28fd1f6c8 --- /dev/null +++ b/activerecord/lib/arel/nodes/insert_statement.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InsertStatement < Arel::Nodes::Node + attr_accessor :relation, :columns, :values, :select + + def initialize + super() + @relation = nil + @columns = [] + @values = nil + @select = nil + end + + def initialize_copy(other) + super + @columns = @columns.clone + @values = @values.clone if @values + @select = @select.clone if @select + end + + def hash + [@relation, @columns, @values, @select].hash + end + + def eql?(other) + self.class == other.class && + self.relation == other.relation && + self.columns == other.columns && + self.select == other.select && + self.values == other.values + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb new file mode 100644 index 0000000000..abf0944623 --- /dev/null +++ b/activerecord/lib/arel/nodes/join_source.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + ### + # Class that represents a join source + # + # http://www.sqlite.org/syntaxdiagrams.html#join-source + + class JoinSource < Arel::Nodes::Binary + def initialize(single_source, joinop = []) + super + end + + def empty? + !left && right.empty? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb new file mode 100644 index 0000000000..fd5734f4bd --- /dev/null +++ b/activerecord/lib/arel/nodes/matches.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Matches < Binary + attr_reader :escape + attr_accessor :case_sensitive + + def initialize(left, right, escape = nil, case_sensitive = false) + super(left, right) + @escape = escape && Nodes.build_quoted(escape) + @case_sensitive = case_sensitive + end + end + + class DoesNotMatch < Matches; end + end +end diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb new file mode 100644 index 0000000000..126462d6d6 --- /dev/null +++ b/activerecord/lib/arel/nodes/named_function.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class NamedFunction < Arel::Nodes::Function + attr_accessor :name + + def initialize(name, expr, aliaz = nil) + super(expr, aliaz) + @name = name + end + + def hash + super ^ @name.hash + end + + def eql?(other) + super && self.name == other.name + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb new file mode 100644 index 0000000000..2b9b1e9828 --- /dev/null +++ b/activerecord/lib/arel/nodes/node.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + ### + # Abstract base class for all AST nodes + class Node + include Arel::FactoryMethods + include Enumerable + + if $DEBUG + def _caller + @caller + end + + def initialize + @caller = caller.dup + end + end + + ### + # Factory method to create a Nodes::Not node that has the recipient of + # the caller as a child. + def not + Nodes::Not.new self + end + + ### + # Factory method to create a Nodes::Grouping node that has an Nodes::Or + # node as a child. + def or(right) + Nodes::Grouping.new Nodes::Or.new(self, right) + end + + ### + # Factory method to create an Nodes::And node. + def and(right) + Nodes::And.new [self, right] + end + + # FIXME: this method should go away. I don't like people calling + # to_sql on non-head nodes. This forces us to walk the AST until we + # can find a node that has a "relation" member. + # + # Maybe we should just use `Table.engine`? :'( + def to_sql(engine = Table.engine) + collector = Arel::Collectors::SQLString.new + collector = engine.connection.visitor.accept self, collector + collector.value + end + + # Iterate through AST, nodes will be yielded depth-first + def each(&block) + return enum_for(:each) unless block_given? + + ::Arel::Visitors::DepthFirst.new(block).accept self + end + end + end +end diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb new file mode 100644 index 0000000000..cbcfaba37c --- /dev/null +++ b/activerecord/lib/arel/nodes/node_expression.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class NodeExpression < Arel::Nodes::Node + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + include Arel::Math + end + end +end diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb new file mode 100644 index 0000000000..0a3042be61 --- /dev/null +++ b/activerecord/lib/arel/nodes/outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class OuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb new file mode 100644 index 0000000000..91176764a9 --- /dev/null +++ b/activerecord/lib/arel/nodes/over.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Over < Binary + include Arel::AliasPredication + + def initialize(left, right = nil) + super(left, right) + end + + def operator; "OVER" end + end + end +end diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb new file mode 100644 index 0000000000..7c25095569 --- /dev/null +++ b/activerecord/lib/arel/nodes/regexp.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Regexp < Binary + attr_accessor :case_sensitive + + def initialize(left, right, case_sensitive = true) + super(left, right) + @case_sensitive = case_sensitive + end + end + + class NotRegexp < Regexp; end + end +end diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb new file mode 100644 index 0000000000..04ed4aaa78 --- /dev/null +++ b/activerecord/lib/arel/nodes/right_outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class RightOuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb new file mode 100644 index 0000000000..2defe61974 --- /dev/null +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SelectCore < Arel::Nodes::Node + attr_accessor :top, :projections, :wheres, :groups, :windows + attr_accessor :havings, :source, :set_quantifier + + def initialize + super() + @source = JoinSource.new nil + @top = nil + + # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier + @set_quantifier = nil + @projections = [] + @wheres = [] + @groups = [] + @havings = [] + @windows = [] + end + + def from + @source.left + end + + def from=(value) + @source.left = value + end + + alias :froms= :from= + alias :froms :from + + def initialize_copy(other) + super + @source = @source.clone if @source + @projections = @projections.clone + @wheres = @wheres.clone + @groups = @groups.clone + @havings = @havings.clone + @windows = @windows.clone + end + + def hash + [ + @source, @top, @set_quantifier, @projections, + @wheres, @groups, @havings, @windows + ].hash + end + + def eql?(other) + self.class == other.class && + self.source == other.source && + self.top == other.top && + self.set_quantifier == other.set_quantifier && + self.projections == other.projections && + self.wheres == other.wheres && + self.groups == other.groups && + self.havings == other.havings && + self.windows == other.windows + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb new file mode 100644 index 0000000000..eff5dad939 --- /dev/null +++ b/activerecord/lib/arel/nodes/select_statement.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SelectStatement < Arel::Nodes::NodeExpression + attr_reader :cores + attr_accessor :limit, :orders, :lock, :offset, :with + + def initialize(cores = [SelectCore.new]) + super() + @cores = cores + @orders = [] + @limit = nil + @lock = nil + @offset = nil + @with = nil + end + + def initialize_copy(other) + super + @cores = @cores.map { |x| x.clone } + @orders = @orders.map { |x| x.clone } + end + + def hash + [@cores, @orders, @limit, @lock, @offset, @with].hash + end + + def eql?(other) + self.class == other.class && + self.cores == other.cores && + self.orders == other.orders && + self.limit == other.limit && + self.lock == other.lock && + self.offset == other.offset && + self.with == other.with + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb new file mode 100644 index 0000000000..d25a8521b7 --- /dev/null +++ b/activerecord/lib/arel/nodes/sql_literal.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SqlLiteral < String + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + + def encode_with(coder) + coder.scalar = self.to_s + end + end + end +end diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb new file mode 100644 index 0000000000..86027fcab7 --- /dev/null +++ b/activerecord/lib/arel/nodes/string_join.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class StringJoin < Arel::Nodes::Join + def initialize(left, right = nil) + super + end + end + end +end diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb new file mode 100644 index 0000000000..f95ca16a3d --- /dev/null +++ b/activerecord/lib/arel/nodes/table_alias.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class TableAlias < Arel::Nodes::Binary + alias :name :right + alias :relation :left + alias :table_alias :name + + def [](name) + Attribute.new(self, name) + end + + def table_name + relation.respond_to?(:name) ? relation.name : name + end + + def type_cast_for_database(*args) + relation.type_cast_for_database(*args) + end + + def able_to_type_cast? + relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb new file mode 100644 index 0000000000..d84c453f1a --- /dev/null +++ b/activerecord/lib/arel/nodes/terminal.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Distinct < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb new file mode 100644 index 0000000000..c891012969 --- /dev/null +++ b/activerecord/lib/arel/nodes/true.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class True < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb new file mode 100644 index 0000000000..a3c0045897 --- /dev/null +++ b/activerecord/lib/arel/nodes/unary.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Unary < Arel::Nodes::NodeExpression + attr_accessor :expr + alias :value :expr + + def initialize(expr) + super() + @expr = expr + end + + def hash + @expr.hash + end + + def eql?(other) + self.class == other.class && + self.expr == other.expr + end + alias :== :eql? + end + + %w{ + Bin + Cube + DistinctOn + Group + GroupingElement + GroupingSet + Lateral + Limit + Lock + Not + Offset + On + Ordering + RollUp + Top + }.each do |name| + const_set(name, Class.new(Unary)) + end + end +end diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb new file mode 100644 index 0000000000..524282ac84 --- /dev/null +++ b/activerecord/lib/arel/nodes/unary_operation.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UnaryOperation < Unary + attr_reader :operator + + def initialize(operator, operand) + super(operand) + @operator = operator + end + end + + class BitwiseNot < UnaryOperation + def initialize(operand) + super(:~, operand) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb new file mode 100644 index 0000000000..7c3e0720d7 --- /dev/null +++ b/activerecord/lib/arel/nodes/unqualified_column.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UnqualifiedColumn < Arel::Nodes::Unary + alias :attribute :expr + alias :attribute= :expr= + + def relation + @expr.relation + end + + def column + @expr.column + end + + def name + @expr.name + end + end + end +end diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb new file mode 100644 index 0000000000..5184b1180f --- /dev/null +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UpdateStatement < Arel::Nodes::Node + attr_accessor :relation, :wheres, :values, :orders, :limit + attr_accessor :key + + def initialize + @relation = nil + @wheres = [] + @values = [] + @orders = [] + @limit = nil + @key = nil + end + + def initialize_copy(other) + super + @wheres = @wheres.clone + @values = @values.clone + end + + def hash + [@relation, @wheres, @values, @orders, @limit, @key].hash + end + + def eql?(other) + self.class == other.class && + self.relation == other.relation && + self.wheres == other.wheres && + self.values == other.values && + self.orders == other.orders && + self.limit == other.limit && + self.key == other.key + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb new file mode 100644 index 0000000000..650248dc04 --- /dev/null +++ b/activerecord/lib/arel/nodes/values.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Values < Arel::Nodes::Binary + alias :expressions :left + alias :expressions= :left= + alias :columns :right + alias :columns= :right= + + def initialize(exprs, columns = []) + super + end + end + end +end diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb new file mode 100644 index 0000000000..27109848e4 --- /dev/null +++ b/activerecord/lib/arel/nodes/values_list.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class ValuesList < Node + attr_reader :rows + + def initialize(rows) + @rows = rows + super() + end + + def hash + @rows.hash + end + + def eql?(other) + self.class == other.class && + self.rows == other.rows + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb new file mode 100644 index 0000000000..4916fc7fbe --- /dev/null +++ b/activerecord/lib/arel/nodes/window.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Window < Arel::Nodes::Node + attr_accessor :orders, :framing, :partitions + + def initialize + @orders = [] + @partitions = [] + @framing = nil + end + + def order(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @orders.concat expr.map { |x| + String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def partition(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @partitions.concat expr.map { |x| + String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def frame(expr) + @framing = expr + end + + def rows(expr = nil) + if @framing + Rows.new(expr) + else + frame(Rows.new(expr)) + end + end + + def range(expr = nil) + if @framing + Range.new(expr) + else + frame(Range.new(expr)) + end + end + + def initialize_copy(other) + super + @orders = @orders.map { |x| x.clone } + end + + def hash + [@orders, @framing].hash + end + + def eql?(other) + self.class == other.class && + self.orders == other.orders && + self.framing == other.framing && + self.partitions == other.partitions + end + alias :== :eql? + end + + class NamedWindow < Window + attr_accessor :name + + def initialize(name) + super() + @name = name + end + + def initialize_copy(other) + super + @name = other.name.clone + end + + def hash + super ^ @name.hash + end + + def eql?(other) + super && self.name == other.name + end + alias :== :eql? + end + + class Rows < Unary + def initialize(expr = nil) + super(expr) + end + end + + class Range < Unary + def initialize(expr = nil) + super(expr) + end + end + + class CurrentRow < Node + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + + class Preceding < Unary + def initialize(expr = nil) + super(expr) + end + end + + class Following < Unary + def initialize(expr = nil) + super(expr) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb new file mode 100644 index 0000000000..157bdcaa08 --- /dev/null +++ b/activerecord/lib/arel/nodes/with.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class With < Arel::Nodes::Unary + alias children expr + end + + class WithRecursive < With; end + end +end diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb new file mode 100644 index 0000000000..d785bbba92 --- /dev/null +++ b/activerecord/lib/arel/order_predications.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module OrderPredications + def asc + Nodes::Ascending.new self + end + + def desc + Nodes::Descending.new self + end + end +end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb new file mode 100644 index 0000000000..e83a6f162f --- /dev/null +++ b/activerecord/lib/arel/predications.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Predications + def not_eq(other) + Nodes::NotEqual.new self, quoted_node(other) + end + + def not_eq_any(others) + grouping_any :not_eq, others + end + + def not_eq_all(others) + grouping_all :not_eq, others + end + + def eq(other) + Nodes::Equality.new self, quoted_node(other) + end + + def eq_any(others) + grouping_any :eq, others + end + + def eq_all(others) + grouping_all :eq, quoted_array(others) + end + + def between(other) + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + not_in([]) + elsif other.exclude_end? + lt(other.end) + else + lteq(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + gteq(other.begin) + elsif other.exclude_end? + gteq(other.begin).and(lt(other.end)) + else + left = quoted_node(other.begin) + right = quoted_node(other.end) + Nodes::Between.new(self, left.and(right)) + end + end + + def in(other) + case other + when Arel::SelectManager + Arel::Nodes::In.new(self, other.ast) + when Range + if $VERBOSE + warn <<-eowarn +Passing a range to `#in` is deprecated. Call `#between`, instead. + eowarn + end + between(other) + when Enumerable + Nodes::In.new self, quoted_array(other) + else + Nodes::In.new self, quoted_node(other) + end + end + + def in_any(others) + grouping_any :in, others + end + + def in_all(others) + grouping_all :in, others + end + + def not_between(other) + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + self.in([]) + elsif other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + lt(other.begin) + else + left = lt(other.begin) + right = if other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + left.or(right) + end + end + + def not_in(other) + case other + when Arel::SelectManager + Arel::Nodes::NotIn.new(self, other.ast) + when Range + if $VERBOSE + warn <<-eowarn +Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. + eowarn + end + not_between(other) + when Enumerable + Nodes::NotIn.new self, quoted_array(other) + else + Nodes::NotIn.new self, quoted_node(other) + end + end + + def not_in_any(others) + grouping_any :not_in, others + end + + def not_in_all(others) + grouping_all :not_in, others + end + + def matches(other, escape = nil, case_sensitive = false) + Nodes::Matches.new self, quoted_node(other), escape, case_sensitive + end + + def matches_regexp(other, case_sensitive = true) + Nodes::Regexp.new self, quoted_node(other), case_sensitive + end + + def matches_any(others, escape = nil, case_sensitive = false) + grouping_any :matches, others, escape, case_sensitive + end + + def matches_all(others, escape = nil, case_sensitive = false) + grouping_all :matches, others, escape, case_sensitive + end + + def does_not_match(other, escape = nil, case_sensitive = false) + Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive + end + + def does_not_match_regexp(other, case_sensitive = true) + Nodes::NotRegexp.new self, quoted_node(other), case_sensitive + end + + def does_not_match_any(others, escape = nil) + grouping_any :does_not_match, others, escape + end + + def does_not_match_all(others, escape = nil) + grouping_all :does_not_match, others, escape + end + + def gteq(right) + Nodes::GreaterThanOrEqual.new self, quoted_node(right) + end + + def gteq_any(others) + grouping_any :gteq, others + end + + def gteq_all(others) + grouping_all :gteq, others + end + + def gt(right) + Nodes::GreaterThan.new self, quoted_node(right) + end + + def gt_any(others) + grouping_any :gt, others + end + + def gt_all(others) + grouping_all :gt, others + end + + def lt(right) + Nodes::LessThan.new self, quoted_node(right) + end + + def lt_any(others) + grouping_any :lt, others + end + + def lt_all(others) + grouping_all :lt, others + end + + def lteq(right) + Nodes::LessThanOrEqual.new self, quoted_node(right) + end + + def lteq_any(others) + grouping_any :lteq, others + end + + def lteq_all(others) + grouping_all :lteq, others + end + + def when(right) + Nodes::Case.new(self).when quoted_node(right) + end + + def concat(other) + Nodes::Concat.new self, other + end + + private + + def grouping_any(method_id, others, *extras) + nodes = others.map { |expr| send(method_id, expr, *extras) } + Nodes::Grouping.new nodes.inject { |memo, node| + Nodes::Or.new(memo, node) + } + end + + def grouping_all(method_id, others, *extras) + nodes = others.map { |expr| send(method_id, expr, *extras) } + Nodes::Grouping.new Nodes::And.new(nodes) + end + + def quoted_node(other) + Nodes.build_quoted(other, self) + end + + def quoted_array(others) + others.map { |v| quoted_node(v) } + end + + def equals_quoted?(maybe_quoted, value) + if maybe_quoted.is_a?(Nodes::Quoted) + maybe_quoted.val == value + else + maybe_quoted == value + end + end + end +end diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb new file mode 100644 index 0000000000..22a04b00c6 --- /dev/null +++ b/activerecord/lib/arel/select_manager.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class SelectManager < Arel::TreeManager + include Arel::Crud + + STRING_OR_SYMBOL_CLASS = [Symbol, String] + + def initialize(table = nil) + super() + @ast = Nodes::SelectStatement.new + @ctx = @ast.cores.last + from table + end + + def initialize_copy(other) + super + @ctx = @ast.cores.last + end + + def limit + @ast.limit && @ast.limit.expr + end + alias :taken :limit + + def constraints + @ctx.wheres + end + + def offset + @ast.offset && @ast.offset.expr + end + + def skip(amount) + if amount + @ast.offset = Nodes::Offset.new(amount) + else + @ast.offset = nil + end + self + end + alias :offset= :skip + + ### + # Produces an Arel::Nodes::Exists node + def exists + Arel::Nodes::Exists.new @ast + end + + def as(other) + create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other) + end + + def lock(locking = Arel.sql("FOR UPDATE")) + case locking + when true + locking = Arel.sql("FOR UPDATE") + when Arel::Nodes::SqlLiteral + when String + locking = Arel.sql locking + end + + @ast.lock = Nodes::Lock.new(locking) + self + end + + def locked + @ast.lock + end + + def on(*exprs) + @ctx.source.right.last.right = Nodes::On.new(collapse(exprs)) + self + end + + def group(*columns) + columns.each do |column| + # FIXME: backwards compat + column = Nodes::SqlLiteral.new(column) if String === column + column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column + + @ctx.groups.push Nodes::Group.new column + end + self + end + + def from(table) + table = Nodes::SqlLiteral.new(table) if String === table + + case table + when Nodes::Join + @ctx.source.right << table + else + @ctx.source.left = table + end + + self + end + + def froms + @ast.cores.map { |x| x.from }.compact + end + + def join(relation, klass = Nodes::InnerJoin) + return self unless relation + + case relation + when String, Nodes::SqlLiteral + raise EmptyJoinError if relation.empty? + klass = Nodes::StringJoin + end + + @ctx.source.right << create_join(relation, nil, klass) + self + end + + def outer_join(relation) + join(relation, Nodes::OuterJoin) + end + + def having(expr) + @ctx.havings << expr + self + end + + def window(name) + window = Nodes::NamedWindow.new(name) + @ctx.windows.push window + window + end + + def project(*projections) + # FIXME: converting these to SQLLiterals is probably not good, but + # rails tests require it. + @ctx.projections.concat projections.map { |x| + STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def projections + @ctx.projections + end + + def projections=(projections) + @ctx.projections = projections + end + + def distinct(value = true) + if value + @ctx.set_quantifier = Arel::Nodes::Distinct.new + else + @ctx.set_quantifier = nil + end + self + end + + def distinct_on(value) + if value + @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value) + else + @ctx.set_quantifier = nil + end + self + end + + def order(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @ast.orders.concat expr.map { |x| + STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def orders + @ast.orders + end + + def where_sql(engine = Table.engine) + return if @ctx.wheres.empty? + + viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection) + Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value + end + + def union(operation, other = nil) + if other + node_class = Nodes.const_get("Union#{operation.to_s.capitalize}") + else + other = operation + node_class = Nodes::Union + end + + node_class.new self.ast, other.ast + end + + def intersect(other) + Nodes::Intersect.new ast, other.ast + end + + def except(other) + Nodes::Except.new ast, other.ast + end + alias :minus :except + + def lateral(table_name = nil) + base = table_name.nil? ? ast : as(table_name) + Nodes::Lateral.new(base) + end + + def with(*subqueries) + if subqueries.first.is_a? Symbol + node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}") + else + node_class = Nodes::With + end + @ast.with = node_class.new(subqueries.flatten) + + self + end + + def take(limit) + if limit + @ast.limit = Nodes::Limit.new(limit) + @ctx.top = Nodes::Top.new(limit) + else + @ast.limit = nil + @ctx.top = nil + end + self + end + alias limit= take + + def join_sources + @ctx.source.right + end + + def source + @ctx.source + end + + class Row < Struct.new(:data) # :nodoc: + def id + data["id"] + end + + def method_missing(name, *args) + name = name.to_s + return data[name] if data.key?(name) + super + end + end + + private + def collapse(exprs, existing = nil) + exprs = exprs.unshift(existing.expr) if existing + exprs = exprs.compact.map { |expr| + if String === expr + # FIXME: Don't do this automatically + Arel.sql(expr) + else + expr + end + } + + if exprs.length == 1 + exprs.first + else + create_and exprs + end + end + end +end diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb new file mode 100644 index 0000000000..686fcdf962 --- /dev/null +++ b/activerecord/lib/arel/table.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class Table + include Arel::Crud + include Arel::FactoryMethods + + @engine = nil + class << self; attr_accessor :engine; end + + attr_accessor :name, :table_alias + + # TableAlias and Table both have a #table_name which is the name of the underlying table + alias :table_name :name + + def initialize(name, as: nil, type_caster: nil) + @name = name.to_s + @type_caster = type_caster + + # Sometime AR sends an :as parameter to table, to let the table know + # that it is an Alias. We may want to override new, and return a + # TableAlias node? + if as.to_s == @name + as = nil + end + @table_alias = as + end + + def alias(name = "#{self.name}_2") + Nodes::TableAlias.new(self, name) + end + + def from + SelectManager.new(self) + end + + def join(relation, klass = Nodes::InnerJoin) + return from unless relation + + case relation + when String, Nodes::SqlLiteral + raise EmptyJoinError if relation.empty? + klass = Nodes::StringJoin + end + + from.join(relation, klass) + end + + def outer_join(relation) + join(relation, Nodes::OuterJoin) + end + + def group(*columns) + from.group(*columns) + end + + def order(*expr) + from.order(*expr) + end + + def where(condition) + from.where condition + end + + def project(*things) + from.project(*things) + end + + def take(amount) + from.take amount + end + + def skip(amount) + from.skip amount + end + + def having(expr) + from.having expr + end + + def [](name) + ::Arel::Attribute.new self, name + end + + def hash + # Perf note: aliases and table alias is excluded from the hash + # aliases can have a loop back to this table breaking hashes in parent + # relations, for the vast majority of cases @name is unique to a query + @name.hash + end + + def eql?(other) + self.class == other.class && + self.name == other.name && + self.table_alias == other.table_alias + end + alias :== :eql? + + def type_cast_for_database(attribute_name, value) + type_caster.type_cast_for_database(attribute_name, value) + end + + def able_to_type_cast? + !type_caster.nil? + end + + protected + + attr_reader :type_caster + end +end diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb new file mode 100644 index 0000000000..ed47b09a37 --- /dev/null +++ b/activerecord/lib/arel/tree_manager.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class TreeManager + include Arel::FactoryMethods + + attr_reader :ast + + def initialize + @ctx = nil + end + + def to_dot + collector = Arel::Collectors::PlainString.new + collector = Visitors::Dot.new.accept @ast, collector + collector.value + end + + def to_sql(engine = Table.engine) + collector = Arel::Collectors::SQLString.new + collector = engine.connection.visitor.accept @ast, collector + collector.value + end + + def initialize_copy(other) + super + @ast = @ast.clone + end + + def where(expr) + if Arel::TreeManager === expr + expr = expr.ast + end + @ctx.wheres << expr + self + end + end +end diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb new file mode 100644 index 0000000000..fe444343ba --- /dev/null +++ b/activerecord/lib/arel/update_manager.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class UpdateManager < Arel::TreeManager + def initialize + super + @ast = Nodes::UpdateStatement.new + @ctx = @ast + end + + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def key=(key) + @ast.key = Nodes.build_quoted(key) + end + + def key + @ast.key + end + + def order(*expr) + @ast.orders = expr + self + end + + ### + # UPDATE +table+ + def table(table) + @ast.relation = table + self + end + + def wheres=(exprs) + @ast.wheres = exprs + end + + def where(expr) + @ast.wheres << expr + self + end + + def set(values) + if String === values + @ast.values = [values] + else + @ast.values = values.map { |column, value| + Nodes::Assignment.new( + Nodes::UnqualifiedColumn.new(column), + value + ) + } + end + self + end + end +end diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb new file mode 100644 index 0000000000..e350f52e65 --- /dev/null +++ b/activerecord/lib/arel/visitors.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "arel/visitors/visitor" +require "arel/visitors/depth_first" +require "arel/visitors/to_sql" +require "arel/visitors/sqlite" +require "arel/visitors/postgresql" +require "arel/visitors/mysql" +require "arel/visitors/mssql" +require "arel/visitors/oracle" +require "arel/visitors/oracle12" +require "arel/visitors/where_sql" +require "arel/visitors/dot" +require "arel/visitors/ibm_db" +require "arel/visitors/informix" + +module Arel # :nodoc: all + module Visitors + end +end diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb new file mode 100644 index 0000000000..bcf8f8f980 --- /dev/null +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class DepthFirst < Arel::Visitors::Visitor + def initialize(block = nil) + @block = block || Proc.new + super() + end + + private + + def visit(o) + super + @block.call o + end + + def unary(o) + visit o.expr + end + alias :visit_Arel_Nodes_Else :unary + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Lateral :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Ordering :unary + alias :visit_Arel_Nodes_Ascending :unary + alias :visit_Arel_Nodes_Descending :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + + def function(o) + visit o.expressions + visit o.alias + visit o.distinct + end + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Sum :function + + def visit_Arel_Nodes_NamedFunction(o) + visit o.name + visit o.expressions + visit o.distinct + visit o.alias + end + + def visit_Arel_Nodes_Count(o) + visit o.expressions + visit o.alias + visit o.distinct + end + + def visit_Arel_Nodes_Case(o) + visit o.case + visit o.conditions + visit o.default + end + + def nary(o) + o.children.each { |child| visit child } + end + alias :visit_Arel_Nodes_And :nary + + def binary(o) + visit o.left + visit o.right + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DeleteStatement :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_FullOuterJoin :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_InfixOperation :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_InnerJoin :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_OuterJoin :binary + alias :visit_Arel_Nodes_Regexp :binary + alias :visit_Arel_Nodes_RightOuterJoin :binary + alias :visit_Arel_Nodes_TableAlias :binary + alias :visit_Arel_Nodes_Values :binary + alias :visit_Arel_Nodes_When :binary + + def visit_Arel_Nodes_StringJoin(o) + visit o.left + end + + def visit_Arel_Attribute(o) + visit o.relation + visit o.name + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute + + def visit_Arel_Table(o) + visit o.name + end + + def terminal(o) + end + alias :visit_ActiveSupport_Multibyte_Chars :terminal + alias :visit_ActiveSupport_StringInquirer :terminal + alias :visit_Arel_Nodes_Lock :terminal + alias :visit_Arel_Nodes_Node :terminal + alias :visit_Arel_Nodes_SqlLiteral :terminal + alias :visit_Arel_Nodes_BindParam :terminal + alias :visit_Arel_Nodes_Window :terminal + alias :visit_Arel_Nodes_True :terminal + alias :visit_Arel_Nodes_False :terminal + alias :visit_BigDecimal :terminal + alias :visit_Bignum :terminal + alias :visit_Class :terminal + alias :visit_Date :terminal + alias :visit_DateTime :terminal + alias :visit_FalseClass :terminal + alias :visit_Fixnum :terminal + alias :visit_Float :terminal + alias :visit_Integer :terminal + alias :visit_NilClass :terminal + alias :visit_String :terminal + alias :visit_Symbol :terminal + alias :visit_Time :terminal + alias :visit_TrueClass :terminal + + def visit_Arel_Nodes_InsertStatement(o) + visit o.relation + visit o.columns + visit o.values + end + + def visit_Arel_Nodes_SelectCore(o) + visit o.projections + visit o.source + visit o.wheres + visit o.groups + visit o.windows + visit o.havings + end + + def visit_Arel_Nodes_SelectStatement(o) + visit o.cores + visit o.orders + visit o.limit + visit o.lock + visit o.offset + end + + def visit_Arel_Nodes_UpdateStatement(o) + visit o.relation + visit o.values + visit o.wheres + visit o.orders + visit o.limit + end + + def visit_Array(o) + o.each { |i| visit i } + end + alias :visit_Set :visit_Array + + def visit_Hash(o) + o.each { |k, v| visit(k); visit(v) } + end + + DISPATCH = dispatch_cache + + def get_dispatch_cache + DISPATCH + end + end + end +end diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb new file mode 100644 index 0000000000..d352b81914 --- /dev/null +++ b/activerecord/lib/arel/visitors/dot.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Dot < Arel::Visitors::Visitor + class Node # :nodoc: + attr_accessor :name, :id, :fields + + def initialize(name, id, fields = []) + @name = name + @id = id + @fields = fields + end + end + + class Edge < Struct.new :name, :from, :to # :nodoc: + end + + def initialize + super() + @nodes = [] + @edges = [] + @node_stack = [] + @edge_stack = [] + @seen = {} + end + + def accept(object, collector) + visit object + collector << to_dot + end + + private + + def visit_Arel_Nodes_Ordering(o) + visit_edge o, "expr" + end + + def visit_Arel_Nodes_TableAlias(o) + visit_edge o, "name" + visit_edge o, "relation" + end + + def visit_Arel_Nodes_Count(o) + visit_edge o, "expressions" + visit_edge o, "distinct" + end + + def visit_Arel_Nodes_Values(o) + visit_edge o, "expressions" + end + + def visit_Arel_Nodes_StringJoin(o) + visit_edge o, "left" + end + + def visit_Arel_Nodes_InnerJoin(o) + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin + + def visit_Arel_Nodes_DeleteStatement(o) + visit_edge o, "relation" + visit_edge o, "wheres" + end + + def unary(o) + visit_edge o, "expr" + end + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + alias :visit_Arel_Nodes_Preceding :unary + alias :visit_Arel_Nodes_Following :unary + alias :visit_Arel_Nodes_Rows :unary + alias :visit_Arel_Nodes_Range :unary + + def window(o) + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + end + alias :visit_Arel_Nodes_Window :window + + def named_window(o) + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + visit_edge o, "name" + end + alias :visit_Arel_Nodes_NamedWindow :named_window + + def function(o) + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Sum :function + + def extract(o) + visit_edge o, "expressions" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Extract :extract + + def visit_Arel_Nodes_NamedFunction(o) + visit_edge o, "name" + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + + def visit_Arel_Nodes_InsertStatement(o) + visit_edge o, "relation" + visit_edge o, "columns" + visit_edge o, "values" + end + + def visit_Arel_Nodes_SelectCore(o) + visit_edge o, "source" + visit_edge o, "projections" + visit_edge o, "wheres" + visit_edge o, "windows" + end + + def visit_Arel_Nodes_SelectStatement(o) + visit_edge o, "cores" + visit_edge o, "limit" + visit_edge o, "orders" + visit_edge o, "offset" + end + + def visit_Arel_Nodes_UpdateStatement(o) + visit_edge o, "relation" + visit_edge o, "wheres" + visit_edge o, "values" + end + + def visit_Arel_Table(o) + visit_edge o, "name" + end + + def visit_Arel_Nodes_Casted(o) + visit_edge o, "val" + visit_edge o, "attribute" + end + + def visit_Arel_Attribute(o) + visit_edge o, "relation" + visit_edge o, "name" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + + def nary(o) + o.children.each_with_index do |x, i| + edge(i) { visit x } + end + end + alias :visit_Arel_Nodes_And :nary + + def binary(o) + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_Over :binary + + def visit_String(o) + @node_stack.last.fields << o + end + alias :visit_Time :visit_String + alias :visit_Date :visit_String + alias :visit_DateTime :visit_String + alias :visit_NilClass :visit_String + alias :visit_TrueClass :visit_String + alias :visit_FalseClass :visit_String + alias :visit_Integer :visit_String + alias :visit_Fixnum :visit_String + alias :visit_BigDecimal :visit_String + alias :visit_Float :visit_String + alias :visit_Symbol :visit_String + alias :visit_Arel_Nodes_SqlLiteral :visit_String + + def visit_Arel_Nodes_BindParam(o); end + + def visit_Hash(o) + o.each_with_index do |pair, i| + edge("pair_#{i}") { visit pair } + end + end + + def visit_Array(o) + o.each_with_index do |x, i| + edge(i) { visit x } + end + end + alias :visit_Set :visit_Array + + def visit_edge(o, method) + edge(method) { visit o.send(method) } + end + + def visit(o) + if node = @seen[o.object_id] + @edge_stack.last.to = node + return + end + + node = Node.new(o.class.name, o.object_id) + @seen[node.id] = node + @nodes << node + with_node node do + super + end + end + + def edge(name) + edge = Edge.new(name, @node_stack.last) + @edge_stack.push edge + @edges << edge + yield + @edge_stack.pop + end + + def with_node(node) + if edge = @edge_stack.last + edge.to = node + end + + @node_stack.push node + yield + @node_stack.pop + end + + def quote(string) + string.to_s.gsub('"', '\"') + end + + def to_dot + "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" + + @nodes.map { |node| + label = "<f0>#{node.name}" + + node.fields.each_with_index do |field, i| + label += "|<f#{i + 1}>#{quote field}" + end + + "#{node.id} [label=\"#{label}\"];" + }.join("\n") + "\n" + @edges.map { |edge| + "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];" + }.join("\n") + "\n}" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb new file mode 100644 index 0000000000..0a06aef60b --- /dev/null +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class IBM_DB < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_Limit(o, collector) + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb new file mode 100644 index 0000000000..0a9713794e --- /dev/null +++ b/activerecord/lib/arel/visitors/informix.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Informix < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_SelectStatement(o, collector) + collector << "SELECT " + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore x, c + } + if o.orders.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + collector = maybe_visit o.lock, collector + end + def visit_Arel_Nodes_SelectCore(o, collector) + collector = inject_join o.projections, collector, ", " + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + if o.wheres.any? + collector << " WHERE " + collector = inject_join o.wheres, collector, " AND " + end + + if o.groups.any? + collector << "GROUP BY " + collector = inject_join o.groups, collector, ", " + end + + if o.havings.any? + collector << " HAVING " + collector = inject_join o.havings, collector, " AND " + end + collector + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "SKIP " + visit o.expr, collector + end + def visit_Arel_Nodes_Limit(o, collector) + collector << "FIRST " + visit o.expr, collector + collector << " " + end + end + end +end diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb new file mode 100644 index 0000000000..9aedc51d15 --- /dev/null +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class MSSQL < Arel::Visitors::ToSql + RowNumber = Struct.new :children + + def initialize(*) + @primary_keys = {} + super + end + + private + + # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate + # "select top 10 distinct first_name from users", which is invalid query! it should be + # "select distinct top 10 first_name from users" + def visit_Arel_Nodes_Top(o) + "" + end + + def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) + collector << "ROW_NUMBER() OVER (ORDER BY " + inject_join(o.children, collector, ", ") << ") as _row_num" + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + if !o.limit && !o.offset + return super + end + + is_select_count = false + o.cores.each { |x| + core_order_by = row_num_literal determine_order_by(o.orders, x) + if select_count? x + x.projections = [core_order_by] + is_select_count = true + else + x.projections << core_order_by + end + } + + if is_select_count + # fixme count distinct wouldn't work with limit or offset + collector << "SELECT COUNT(1) as count_id FROM (" + end + + collector << "SELECT _t.* FROM (" + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore x, c + } + collector << ") as _t WHERE #{get_offset_limit_clause(o)}" + + if is_select_count + collector << ") AS subquery" + else + collector + end + end + + def get_offset_limit_clause(o) + first_row = o.offset ? o.offset.expr.to_i + 1 : 1 + last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil + if last_row + " _row_num BETWEEN #{first_row} AND #{last_row}" + else + " _row_num >= #{first_row}" + end + end + + def visit_Arel_Nodes_DeleteStatement(o, collector) + collector << "DELETE " + if o.limit + collector << "TOP (" + visit o.limit.expr, collector + collector << ") " + end + collector << "FROM " + collector = visit o.relation, collector + if o.wheres.any? + collector << " WHERE " + inject_join o.wheres, collector, AND + else + collector + end + end + + def determine_order_by(orders, x) + if orders.any? + orders + elsif x.groups.any? + x.groups + else + pk = find_left_table_pk(x.froms) + pk ? [pk] : [] + end + end + + def row_num_literal(order_by) + RowNumber.new order_by + end + + def select_count?(x) + x.projections.length == 1 && Arel::Nodes::Count === x.projections.first + end + + # FIXME raise exception of there is no pk? + def find_left_table_pk(o) + if o.kind_of?(Arel::Nodes::Join) + find_left_table_pk(o.left) + elsif o.instance_of?(Arel::Table) + find_primary_key(o) + end + end + + def find_primary_key(o) + @primary_keys[o.name] ||= begin + primary_key_name = @connection.primary_key(o.name) + # some tables might be without primary key + primary_key_name && o[primary_key_name] + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb new file mode 100644 index 0000000000..37bfb661f0 --- /dev/null +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class MySQL < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_Union(o, collector, suppress_parens = false) + unless suppress_parens + collector << "( " + end + + collector = case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector + end + + collector << " UNION " + + collector = case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector + end + + if suppress_parens + collector + else + collector << " )" + end + end + + def visit_Arel_Nodes_Bin(o, collector) + collector << "BINARY " + visit o.expr, collector + end + + ### + # :'( + # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 + def visit_Arel_Nodes_SelectStatement(o, collector) + if o.offset && !o.limit + o.limit = Arel::Nodes::Limit.new(18446744073709551615) + end + super + end + + def visit_Arel_Nodes_SelectCore(o, collector) + o.froms ||= Arel.sql("DUAL") + super + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + collector << "UPDATE " + collector = visit o.relation, collector + + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ", " + end + + unless o.wheres.empty? + collector << " WHERE " + collector = inject_join o.wheres, collector, " AND " + end + + unless o.orders.empty? + collector << " ORDER BY " + collector = inject_join o.orders, collector, ", " + end + + maybe_visit o.limit, collector + end + + def visit_Arel_Nodes_Concat(o, collector) + collector << " CONCAT(" + visit o.left, collector + collector << ", " + visit o.right, collector + collector << ") " + collector + end + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb new file mode 100644 index 0000000000..30a1529d46 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Oracle < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement(o, collector) + o = order_hacks(o) + + # if need to select first records without ORDER BY and GROUP BY and without DISTINCT + # then can use simple ROWNUM in WHERE clause + if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/ + o.cores.last.wheres.push Nodes::LessThanOrEqual.new( + Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr + ) + return super + end + + if o.limit && o.offset + o = o.dup + limit = o.limit.expr + offset = o.offset + o.offset = nil + collector << " + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + + collector = super(o, collector) + + if offset.expr.is_a? Nodes::BindParam + collector << ") raw_sql_ WHERE rownum <= (" + collector = visit offset.expr, collector + collector << " + " + collector = visit limit, collector + collector << ") ) WHERE raw_rnum_ > " + collector = visit offset.expr, collector + return collector + else + collector << ") raw_sql_ + WHERE rownum <= #{offset.expr.to_i + limit} + ) + WHERE " + return visit(offset, collector) + end + end + + if o.limit + o = o.dup + limit = o.limit.expr + collector << "SELECT * FROM (" + collector = super(o, collector) + collector << ") WHERE ROWNUM <= " + return visit limit, collector + end + + if o.offset + o = o.dup + offset = o.offset + o.offset = nil + collector << "SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + collector = super(o, collector) + collector << ") raw_sql_ + ) + WHERE " + return visit offset, collector + end + + super + end + + def visit_Arel_Nodes_Limit(o, collector) + collector + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "raw_rnum_ > " + visit o.expr, collector + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + ### + # Hacks for the order clauses specific to Oracle + def order_hacks(o) + return o if o.orders.empty? + return o unless o.cores.any? do |core| + core.projections.any? do |projection| + /FIRST_VALUE/ === projection + end + end + # Previous version with join and split broke ORDER BY clause + # if it contained functions with several arguments (separated by ','). + # + # orders = o.orders.map { |x| visit x }.join(', ').split(',') + orders = o.orders.map do |x| + string = visit(x, Arel::Collectors::SQLString.new).value + if string.include?(",") + split_order_string(string) + else + string + end + end.flatten + o.orders = [] + orders.each_with_index do |order, i| + o.orders << + Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}") + end + o + end + + # Split string by commas but count opening and closing brackets + # and ignore commas inside brackets. + def split_order_string(string) + array = [] + i = 0 + string.split(",").each do |part| + if array[i] + array[i] << "," << part + else + # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral + array[i] = part.to_s + end + i += 1 if array[i].count("(") == array[i].count(")") + end + array + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| ":a#{i}" } + end + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb new file mode 100644 index 0000000000..7061f06087 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Oracle12 < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement(o, collector) + # Oracle does not allow LIMIT clause with select for update + if o.limit && o.lock + raise ArgumentError, <<-MSG + 'Combination of limit and lock is not supported. + because generated SQL statements + `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.` + MSG + end + super + end + + def visit_Arel_Nodes_SelectOptions(o, collector) + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_Limit(o, collector) + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "OFFSET " + visit o.expr, collector + collector << " ROWS" + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| ":a#{i}" } + end + end + end +end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb new file mode 100644 index 0000000000..c5110fa89c --- /dev/null +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class PostgreSQL < Arel::Visitors::ToSql + CUBE = "CUBE" + ROLLUP = "ROLLUP" + GROUPING_SETS = "GROUPING SETS" + LATERAL = "LATERAL" + + private + + def visit_Arel_Nodes_Matches(o, collector) + op = o.case_sensitive ? " LIKE " : " ILIKE " + collector = infix_value o, collector, op + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch(o, collector) + op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE " + collector = infix_value o, collector, op + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_Regexp(o, collector) + op = o.case_sensitive ? " ~ " : " ~* " + infix_value o, collector, op + end + + def visit_Arel_Nodes_NotRegexp(o, collector) + op = o.case_sensitive ? " !~ " : " !~* " + infix_value o, collector, op + end + + def visit_Arel_Nodes_DistinctOn(o, collector) + collector << "DISTINCT ON ( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| "$#{i}" } + end + + def visit_Arel_Nodes_GroupingElement(o, collector) + collector << "( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_Cube(o, collector) + collector << CUBE + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_RollUp(o, collector) + collector << ROLLUP + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_GroupingSet(o, collector) + collector << GROUPING_SETS + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_Lateral(o, collector) + collector << LATERAL + collector << SPACE + grouping_parentheses o, collector + end + + # Used by Lateral visitor to enclose select queries in parentheses + def grouping_parentheses(o, collector) + if o.expr.is_a? Nodes::SelectStatement + collector << "(" + visit o.expr, collector + collector << ")" + else + visit o.expr, collector + end + end + + # Utilized by GroupingSet, Cube & RollUp visitors to + # handle grouping aggregation semantics + def grouping_array_or_grouping_element(o, collector) + if o.expr.is_a? Array + collector << "( " + visit o.expr, collector + collector << " )" + else + visit o.expr, collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb new file mode 100644 index 0000000000..cb1d2424ad --- /dev/null +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class SQLite < Arel::Visitors::ToSql + private + + # Locks are not supported in SQLite + def visit_Arel_Nodes_Lock(o, collector) + collector + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit + super + end + + def visit_Arel_Nodes_True(o, collector) + collector << "1" + end + + def visit_Arel_Nodes_False(o, collector) + collector << "0" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb new file mode 100644 index 0000000000..5986fd5576 --- /dev/null +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -0,0 +1,847 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class UnsupportedVisitError < StandardError + def initialize(object) + super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead." + end + end + + class ToSql < Arel::Visitors::Visitor + ## + # This is some roflscale crazy stuff. I'm roflscaling this because + # building SQL queries is a hotspot. I will explain the roflscale so that + # others will not rm this code. + # + # In YARV, string literals in a method body will get duped when the byte + # code is executed. Let's take a look: + # + # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>===== + # 0000 trace 8 + # 0002 trace 1 + # 0004 putstring "bar" + # 0006 trace 16 + # 0008 leave + # + # The `putstring` bytecode will dup the string and push it on the stack. + # In many cases in our SQL visitor, that string is never mutated, so there + # is no need to dup the literal. + # + # If we change to a constant lookup, the string will not be duped, and we + # can reduce the objects in our system: + # + # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>======== + # 0000 trace 8 + # 0002 trace 1 + # 0004 getinlinecache 11, <ic:0> + # 0007 getconstant :BAR + # 0009 setinlinecache <ic:0> + # 0011 trace 16 + # 0013 leave + # + # `getconstant` should be a hash lookup, and no object is duped when the + # value of the constant is pushed on the stack. Hence the crazy + # constants below. + # + # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses + # specialized for specific databases when necessary. + # + + WHERE = " WHERE " # :nodoc: + SPACE = " " # :nodoc: + COMMA = ", " # :nodoc: + GROUP_BY = " GROUP BY " # :nodoc: + ORDER_BY = " ORDER BY " # :nodoc: + WINDOW = " WINDOW " # :nodoc: + AND = " AND " # :nodoc: + + DISTINCT = "DISTINCT" # :nodoc: + + def initialize(connection) + super() + @connection = connection + end + + def compile(node, &block) + accept(node, Arel::Collectors::SQLString.new, &block).value + end + + private + + def visit_Arel_Nodes_DeleteStatement(o, collector) + collector << "DELETE FROM " + collector = visit o.relation, collector + if o.wheres.any? + collector << WHERE + collector = inject_join o.wheres, collector, AND + end + + maybe_visit o.limit, collector + end + + # FIXME: we should probably have a 2-pass visitor for this + def build_subselect(key, o) + stmt = Nodes::SelectStatement.new + core = stmt.cores.first + core.froms = o.relation + core.wheres = o.wheres + core.projections = [key] + stmt.limit = o.limit + stmt.orders = o.orders + stmt + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + if o.orders.empty? && o.limit.nil? + wheres = o.wheres + else + wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] + end + + collector << "UPDATE " + collector = visit o.relation, collector + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ", " + end + + unless wheres.empty? + collector << " WHERE " + collector = inject_join wheres, collector, " AND " + end + + collector + end + + def visit_Arel_Nodes_InsertStatement(o, collector) + collector << "INSERT INTO " + collector = visit o.relation, collector + if o.columns.any? + collector << " (#{o.columns.map { |x| + quote_column_name x.name + }.join ', '})" + end + + if o.values + maybe_visit o.values, collector + elsif o.select + maybe_visit o.select, collector + else + collector + end + end + + def visit_Arel_Nodes_Exists(o, collector) + collector << "EXISTS (" + collector = visit(o.expressions, collector) << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Casted(o, collector) + collector << quoted(o.val, o.attribute).to_s + end + + def visit_Arel_Nodes_Quoted(o, collector) + collector << quoted(o.expr, nil).to_s + end + + def visit_Arel_Nodes_True(o, collector) + collector << "TRUE" + end + + def visit_Arel_Nodes_False(o, collector) + collector << "FALSE" + end + + def visit_Arel_Nodes_ValuesList(o, collector) + collector << "VALUES " + + len = o.rows.length - 1 + o.rows.each_with_index { |row, i| + collector << "(" + row_len = row.length - 1 + row.each_with_index do |value, k| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit(value, collector) + else + collector << quote(value) + end + collector << COMMA unless k == row_len + end + collector << ")" + collector << COMMA unless i == len + } + collector + end + + def visit_Arel_Nodes_Values(o, collector) + collector << "VALUES (" + + len = o.expressions.length - 1 + o.expressions.each_with_index { |value, i| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit value, collector + else + collector << quote(value).to_s + end + unless i == len + collector << COMMA + end + } + + collector << ")" + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + if o.with + collector = visit o.with, collector + collector << SPACE + end + + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore(x, c) + } + + unless o.orders.empty? + collector << ORDER_BY + len = o.orders.length - 1 + o.orders.each_with_index { |x, i| + collector = visit(x, collector) + collector << COMMA unless len == i + } + end + + visit_Arel_Nodes_SelectOptions(o, collector) + + collector + end + + def visit_Arel_Nodes_SelectOptions(o, collector) + collector = maybe_visit o.limit, collector + collector = maybe_visit o.offset, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_SelectCore(o, collector) + collector << "SELECT" + + collector = maybe_visit o.top, collector + + collector = maybe_visit o.set_quantifier, collector + + collect_nodes_for o.projections, collector, SPACE + + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + collect_nodes_for o.wheres, collector, WHERE, AND + collect_nodes_for o.groups, collector, GROUP_BY + unless o.havings.empty? + collector << " HAVING " + inject_join o.havings, collector, AND + end + collect_nodes_for o.windows, collector, WINDOW + + collector + end + + def collect_nodes_for(nodes, collector, spacer, connector = COMMA) + unless nodes.empty? + collector << spacer + len = nodes.length - 1 + nodes.each_with_index do |x, i| + collector = visit(x, collector) + collector << connector unless len == i + end + end + end + + def visit_Arel_Nodes_Bin(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_Distinct(o, collector) + collector << DISTINCT + end + + def visit_Arel_Nodes_DistinctOn(o, collector) + raise NotImplementedError, "DISTINCT ON not implemented for this db" + end + + def visit_Arel_Nodes_With(o, collector) + collector << "WITH " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_WithRecursive(o, collector) + collector << "WITH RECURSIVE " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_Union(o, collector) + collector << "( " + infix_value(o, collector, " UNION ") << " )" + end + + def visit_Arel_Nodes_UnionAll(o, collector) + collector << "( " + infix_value(o, collector, " UNION ALL ") << " )" + end + + def visit_Arel_Nodes_Intersect(o, collector) + collector << "( " + infix_value(o, collector, " INTERSECT ") << " )" + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + infix_value(o, collector, " EXCEPT ") << " )" + end + + def visit_Arel_Nodes_NamedWindow(o, collector) + collector << quote_column_name(o.name) + collector << " AS " + visit_Arel_Nodes_Window o, collector + end + + def visit_Arel_Nodes_Window(o, collector) + collector << "(" + + if o.partitions.any? + collector << "PARTITION BY " + collector = inject_join o.partitions, collector, ", " + end + + if o.orders.any? + collector << SPACE if o.partitions.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + + if o.framing + collector << SPACE if o.partitions.any? || o.orders.any? + collector = visit o.framing, collector + end + + collector << ")" + end + + def visit_Arel_Nodes_Rows(o, collector) + if o.expr + collector << "ROWS " + visit o.expr, collector + else + collector << "ROWS" + end + end + + def visit_Arel_Nodes_Range(o, collector) + if o.expr + collector << "RANGE " + visit o.expr, collector + else + collector << "RANGE" + end + end + + def visit_Arel_Nodes_Preceding(o, collector) + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " PRECEDING" + end + + def visit_Arel_Nodes_Following(o, collector) + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " FOLLOWING" + end + + def visit_Arel_Nodes_CurrentRow(o, collector) + collector << "CURRENT ROW" + end + + def visit_Arel_Nodes_Over(o, collector) + case o.right + when nil + visit(o.left, collector) << " OVER ()" + when Arel::Nodes::SqlLiteral + infix_value o, collector, " OVER " + when String, Symbol + visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}" + else + infix_value o, collector, " OVER " + end + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "OFFSET " + visit o.expr, collector + end + + def visit_Arel_Nodes_Limit(o, collector) + collector << "LIMIT " + visit o.expr, collector + end + + # FIXME: this does nothing on most databases, but does on MSSQL + def visit_Arel_Nodes_Top(o, collector) + collector + end + + def visit_Arel_Nodes_Lock(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_Grouping(o, collector) + if o.expr.is_a? Nodes::Grouping + visit(o.expr, collector) + else + collector << "(" + visit(o.expr, collector) << ")" + end + end + + def visit_Arel_SelectManager(o, collector) + collector << "(" + visit(o.ast, collector) << ")" + end + + def visit_Arel_Nodes_Ascending(o, collector) + visit(o.expr, collector) << " ASC" + end + + def visit_Arel_Nodes_Descending(o, collector) + visit(o.expr, collector) << " DESC" + end + + def visit_Arel_Nodes_Group(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_NamedFunction(o, collector) + collector << o.name + collector << "(" + collector << "DISTINCT " if o.distinct + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Extract(o, collector) + collector << "EXTRACT(#{o.field.to_s.upcase} FROM " + visit(o.expr, collector) << ")" + end + + def visit_Arel_Nodes_Count(o, collector) + aggregate "COUNT", o, collector + end + + def visit_Arel_Nodes_Sum(o, collector) + aggregate "SUM", o, collector + end + + def visit_Arel_Nodes_Max(o, collector) + aggregate "MAX", o, collector + end + + def visit_Arel_Nodes_Min(o, collector) + aggregate "MIN", o, collector + end + + def visit_Arel_Nodes_Avg(o, collector) + aggregate "AVG", o, collector + end + + def visit_Arel_Nodes_TableAlias(o, collector) + collector = visit o.relation, collector + collector << " " + collector << quote_table_name(o.name) + end + + def visit_Arel_Nodes_Between(o, collector) + collector = visit o.left, collector + collector << " BETWEEN " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThanOrEqual(o, collector) + collector = visit o.left, collector + collector << " >= " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThan(o, collector) + collector = visit o.left, collector + collector << " > " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThanOrEqual(o, collector) + collector = visit o.left, collector + collector << " <= " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThan(o, collector) + collector = visit o.left, collector + collector << " < " + visit o.right, collector + end + + def visit_Arel_Nodes_Matches(o, collector) + collector = visit o.left, collector + collector << " LIKE " + collector = visit o.right, collector + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch(o, collector) + collector = visit o.left, collector + collector << " NOT LIKE " + collector = visit o.right, collector + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_JoinSource(o, collector) + if o.left + collector = visit o.left, collector + end + if o.right.any? + collector << SPACE if o.left + collector = inject_join o.right, collector, SPACE + end + collector + end + + def visit_Arel_Nodes_Regexp(o, collector) + raise NotImplementedError, "~ not implemented for this db" + end + + def visit_Arel_Nodes_NotRegexp(o, collector) + raise NotImplementedError, "!~ not implemented for this db" + end + + def visit_Arel_Nodes_StringJoin(o, collector) + visit o.left, collector + end + + def visit_Arel_Nodes_FullOuterJoin(o, collector) + collector << "FULL OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_OuterJoin(o, collector) + collector << "LEFT OUTER JOIN " + collector = visit o.left, collector + collector << " " + visit o.right, collector + end + + def visit_Arel_Nodes_RightOuterJoin(o, collector) + collector << "RIGHT OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_InnerJoin(o, collector) + collector << "INNER JOIN " + collector = visit o.left, collector + if o.right + collector << SPACE + visit(o.right, collector) + else + collector + end + end + + def visit_Arel_Nodes_On(o, collector) + collector << "ON " + visit o.expr, collector + end + + def visit_Arel_Nodes_Not(o, collector) + collector << "NOT (" + visit(o.expr, collector) << ")" + end + + def visit_Arel_Table(o, collector) + if o.table_alias + collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}" + else + collector << quote_table_name(o.name) + end + end + + def visit_Arel_Nodes_In(o, collector) + if Array === o.right && o.right.empty? + collector << "1=0" + else + collector = visit o.left, collector + collector << " IN (" + visit(o.right, collector) << ")" + end + end + + def visit_Arel_Nodes_NotIn(o, collector) + if Array === o.right && o.right.empty? + collector << "1=1" + else + collector = visit o.left, collector + collector << " NOT IN (" + collector = visit o.right, collector + collector << ")" + end + end + + def visit_Arel_Nodes_And(o, collector) + inject_join o.children, collector, " AND " + end + + def visit_Arel_Nodes_Or(o, collector) + collector = visit o.left, collector + collector << " OR " + visit o.right, collector + end + + def visit_Arel_Nodes_Assignment(o, collector) + case o.right + when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + collector = visit o.left, collector + collector << " = " + visit o.right, collector + else + collector = visit o.left, collector + collector << " = " + collector << quote(o.right).to_s + end + end + + def visit_Arel_Nodes_Equality(o, collector) + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NULL" + else + collector << " = " + visit right, collector + end + end + + def visit_Arel_Nodes_NotEqual(o, collector) + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NOT NULL" + else + collector << " != " + visit right, collector + end + end + + def visit_Arel_Nodes_As(o, collector) + collector = visit o.left, collector + collector << " AS " + visit o.right, collector + end + + def visit_Arel_Nodes_Case(o, collector) + collector << "CASE " + if o.case + visit o.case, collector + collector << " " + end + o.conditions.each do |condition| + visit condition, collector + collector << " " + end + if o.default + visit o.default, collector + collector << " " + end + collector << "END" + end + + def visit_Arel_Nodes_When(o, collector) + collector << "WHEN " + visit o.left, collector + collector << " THEN " + visit o.right, collector + end + + def visit_Arel_Nodes_Else(o, collector) + collector << "ELSE " + visit o.expr, collector + end + + def visit_Arel_Nodes_UnqualifiedColumn(o, collector) + collector << "#{quote_column_name o.name}" + collector + end + + def visit_Arel_Attributes_Attribute(o, collector) + join_name = o.relation.table_alias || o.relation.name + collector << "#{quote_table_name join_name}.#{quote_column_name o.name}" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute + + def literal(o, collector); collector << o.to_s; end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { "?" } + end + + alias :visit_Arel_Nodes_SqlLiteral :literal + alias :visit_Bignum :literal + alias :visit_Fixnum :literal + alias :visit_Integer :literal + + def quoted(o, a) + if a && a.able_to_type_cast? + quote(a.type_cast_for_database(o)) + else + quote(o) + end + end + + def unsupported(o, collector) + raise UnsupportedVisitError.new(o) + end + + alias :visit_ActiveSupport_Multibyte_Chars :unsupported + alias :visit_ActiveSupport_StringInquirer :unsupported + alias :visit_BigDecimal :unsupported + alias :visit_Class :unsupported + alias :visit_Date :unsupported + alias :visit_DateTime :unsupported + alias :visit_FalseClass :unsupported + alias :visit_Float :unsupported + alias :visit_Hash :unsupported + alias :visit_NilClass :unsupported + alias :visit_String :unsupported + alias :visit_Symbol :unsupported + alias :visit_Time :unsupported + alias :visit_TrueClass :unsupported + + def visit_Arel_Nodes_InfixOperation(o, collector) + collector = visit o.left, collector + collector << " #{o.operator} " + visit o.right, collector + end + + alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation + + def visit_Arel_Nodes_UnaryOperation(o, collector) + collector << " #{o.operator} " + visit o.expr, collector + end + + def visit_Array(o, collector) + inject_join o, collector, ", " + end + alias :visit_Set :visit_Array + + def quote(value) + return value if Arel::Nodes::SqlLiteral === value + @connection.quote value + end + + def quote_table_name(name) + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_table_name(name) + end + + def quote_column_name(name) + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_column_name(name) + end + + def maybe_visit(thing, collector) + return collector unless thing + collector << " " + visit thing, collector + end + + def inject_join(list, collector, join_str) + len = list.length - 1 + list.each_with_index.inject(collector) { |c, (x, i)| + if i == len + visit x, c + else + visit(x, c) << join_str + end + } + end + + def infix_value(o, collector, value) + collector = visit o.left, collector + collector << value + visit o.right, collector + end + + def aggregate(name, o, collector) + collector << "#{name}(" + if o.distinct + collector << "DISTINCT " + end + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb new file mode 100644 index 0000000000..1c17184e86 --- /dev/null +++ b/activerecord/lib/arel/visitors/visitor.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Visitor + def initialize + @dispatch = get_dispatch_cache + end + + def accept(object, *args) + visit object, *args + end + + private + + attr_reader :dispatch + + def self.dispatch_cache + Hash.new do |hash, klass| + hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}" + end + end + + def get_dispatch_cache + self.class.dispatch_cache + end + + def visit(object, *args) + dispatch_method = dispatch[object.class] + send dispatch_method, object, *args + rescue NoMethodError => e + raise e if respond_to?(dispatch_method, true) + superklass = object.class.ancestors.find { |klass| + respond_to?(dispatch[klass], true) + } + raise(TypeError, "Cannot visit #{object.class}") unless superklass + dispatch[object.class] = dispatch[superklass] + retry + end + end + end +end diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb new file mode 100644 index 0000000000..c6caf5e7c9 --- /dev/null +++ b/activerecord/lib/arel/visitors/where_sql.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class WhereSql < Arel::Visitors::ToSql + def initialize(inner_visitor, *args, &block) + @inner_visitor = inner_visitor + super(*args, &block) + end + + private + + def visit_Arel_Nodes_SelectCore(o, collector) + collector << "WHERE " + wheres = o.wheres.map do |where| + Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value) + end + + inject_join wheres, collector, " AND " + end + end + end +end diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb new file mode 100644 index 0000000000..3a8ee41f8a --- /dev/null +++ b/activerecord/lib/arel/window_predications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module WindowPredications + def over(expr = nil) + Nodes::Over.new(self, expr) + end + 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 856fcc5897..a07b00ef79 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -15,12 +15,8 @@ module ActiveRecord migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - attr_reader :migration_action, :join_tables - private + attr_reader :migration_action, :join_tables # Sets the default migration template that is being used for the generation of the migration. # Depending on command line arguments, the migration template and the table name instance diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore deleted file mode 100644 index a0ec5967dd..0000000000 --- a/activerecord/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/config.yml diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 9aaa2852d0..59b99351d1 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -20,7 +20,7 @@ module ActiveRecord b = Book.create(name: "my \x00 book") b.reload assert_equal "my \x00 book", b.name - b.update_attributes(name: "my other \x00 book") + b.update(name: "my other \x00 book") b.reload assert_equal "my other \x00 book", b.name end @@ -78,13 +78,13 @@ module ActiveRecord idx_name = "accounts_idx" indexes = @connection.indexes("accounts") - assert indexes.empty? + assert_empty indexes @connection.add_index :accounts, :firm_id, name: idx_name indexes = @connection.indexes("accounts") assert_equal "accounts", indexes.first.table assert_equal idx_name, indexes.first.name - assert !indexes.first.unique + assert_not indexes.first.unique assert_equal ["firm_id"], indexes.first.columns ensure @connection.remove_index(:accounts, name: idx_name) rescue nil @@ -295,11 +295,17 @@ module ActiveRecord assert_equal "ы", error.message end end + + def test_supports_multi_insert_is_deprecated + assert_deprecated { @connection.supports_multi_insert? } + end end class AdapterForeignKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false + fixtures :fk_test_has_pk + def setup @connection = ActiveRecord::Base.connection end @@ -318,7 +324,7 @@ module ActiveRecord assert_not_nil error.cause end - def test_foreign_key_violations_are_translated_to_specific_exception + def test_foreign_key_violations_on_insert_are_translated_to_specific_exception error = assert_raises(ActiveRecord::InvalidForeignKey) do insert_into_fk_test_has_fk end @@ -326,6 +332,16 @@ module ActiveRecord assert_not_nil error.cause end + def test_foreign_key_violations_on_delete_are_translated_to_specific_exception + insert_into_fk_test_has_fk fk_id: 1 + + error = assert_raises(ActiveRecord::InvalidForeignKey) do + @connection.execute "DELETE FROM fk_test_has_pk WHERE pk_id = 1" + end + + assert_not_nil error.cause + end + def test_disable_referential_integrity assert_nothing_raised do @connection.disable_referential_integrity do @@ -338,14 +354,13 @@ module ActiveRecord end private - - def insert_into_fk_test_has_fk + def insert_into_fk_test_has_fk(fk_id: 0) # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if @connection.prefetch_primary_key? id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)" + @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},#{fk_id})" else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (#{fk_id})" end end end @@ -368,16 +383,16 @@ module ActiveRecord unless in_memory_db? test "transaction state is reset after a reconnect" do @connection.begin_transaction - assert @connection.transaction_open? + assert_predicate @connection, :transaction_open? @connection.reconnect! - assert !@connection.transaction_open? + assert_not_predicate @connection, :transaction_open? end test "transaction state is reset after a disconnect" do @connection.begin_transaction - assert @connection.transaction_open? + assert_predicate @connection, :transaction_open? @connection.disconnect! - assert !@connection.transaction_open? + assert_not_predicate @connection, :transaction_open? end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 6931b085a8..6fc9df5083 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -108,7 +108,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_create_mysql_database_with_encoding assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, charset: :big5, collation: :big5_chinese_ci) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end def test_recreate_mysql_database_with_encoding @@ -148,8 +148,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true - assert !column_present?("delete_me", "updated_at", "datetime") - assert !column_present?("delete_me", "created_at", "datetime") + assert_not column_present?("delete_me", "updated_at", "datetime") + assert_not column_present?("delete_me", "created_at", "datetime") ensure ActiveRecord::Base.connection.drop_table :delete_me rescue nil end @@ -157,15 +157,19 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) + assert_called_with( + ActiveRecord::Base.connection, + :data_source_exists?, + [:temp], + returns: false + ) do + expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) 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 - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) 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 + assert_equal expected, actual end - - assert_equal expected, actual end private diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index fd5f712f1a..aa870349be 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -14,8 +14,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase end def test_case_sensitive - assert !CollationTest.columns_hash["string_ci_column"].case_sensitive? - assert CollationTest.columns_hash["string_cs_column"].case_sensitive? + assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive? + assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive? end def test_case_insensitive_comparison_for_ci_column diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 13b4096671..726f58d58e 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -40,29 +40,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_no_automatic_reconnection_after_timeout - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 - assert !@connection.active? + assert_not_predicate @connection, :active? ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 @connection.reconnect! - assert @connection.active? + assert_predicate @connection, :active? end def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 @connection.verify! - assert @connection.active? + assert_predicate @connection, :active? end def test_execute_after_disconnect diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index fa54f39992..00a075e063 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -45,9 +45,10 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase end def stub_version(full_version_string) - @connection.stubs(:full_version).returns(full_version_string) - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - yield + @connection.stub(:full_version, full_version_string) do + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + end ensure @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 108bec832c..832f5d61d1 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -13,11 +13,11 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase def test_should_not_be_unsigned column = EnumTest.columns_hash["enum_column"] - assert_not column.unsigned? + assert_not_predicate column, :unsigned? end def test_should_not_be_bigint column = EnumTest.columns_hash["enum_column"] - assert_not column.bigint? + assert_not_predicate column, :bigint? end end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index d18fb97e05..0719baaa23 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -25,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end def test_columns_for_distinct_one_order - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc"]) end def test_columns_for_distinct_few_orders - assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end def test_columns_for_distinct_with_case assert_equal( - "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0", + "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) ) end def test_columns_for_distinct_blank_not_nil_orders - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end @@ -52,7 +52,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase def order.to_sql "posts.created_at desc" end - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", [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 62abd694bb..d7d9a2d732 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -36,7 +36,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase assert connection.column_exists?(table_name, :key, :string) end ensure - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index b587e756cf..1283b0642c 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -67,7 +67,7 @@ module ActiveRecord end def test_data_source_exists_wrong_schema - assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") + assert_not(@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index f921515c10..52e283f247 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -13,7 +13,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false - Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -32,7 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort - Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = @original_report_on_exception end test "raises Deadlocked when a deadlock is encountered" do @@ -46,7 +46,7 @@ module ActiveRecord Sample.transaction do s1.lock! barrier.wait - s2.update_attributes value: 1 + s2.update value: 1 end end @@ -54,7 +54,7 @@ module ActiveRecord Sample.transaction do s2.lock! barrier.wait - s1.update_attributes value: 2 + s1.update value: 2 end ensure thread.join diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index b01f5d7f5a..97da96003d 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -54,7 +54,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase end @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column| - assert column.unsigned? + assert_predicate column, :unsigned? end end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb index ffde8ed4d8..8494acee3b 100644 --- a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -18,6 +18,7 @@ if ActiveRecord::Base.connection.supports_virtual_columns? t.string :name t.virtual :upper_name, type: :string, as: "UPPER(`name`)" t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true + t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(`name`)", stored: true end VirtualColumn.create(name: "Rails") end @@ -55,7 +56,8 @@ if ActiveRecord::Base.connection.supports_virtual_columns? def test_schema_dumping output = dump_table_schema("virtual_columns") assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "(?:UPPER|UCASE)\(`name`\)"$/i, output) - assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) + assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output) + assert_match(/t\.virtual\s+"name_octet_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output) end end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 99c53dadeb..308ad1d854 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -62,6 +62,12 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops)) assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops }) + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc }) + + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST)) + assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST") + assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :copy) end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 0e9e86f425..42618c2ec3 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -39,12 +39,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :string, @column.type assert_equal "character varying(255)", @column.sql_type - assert @column.array? - assert_not @type.binary? + assert_predicate @column, :array? + assert_not_predicate @type, :binary? ratings_column = PgArray.columns_hash["ratings"] assert_equal :integer, ratings_column.type - assert ratings_column.array? + assert_predicate ratings_column, :array? end def test_not_compatible_with_serialize_array @@ -109,7 +109,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal :text, column.type assert_equal [], PgArray.column_defaults["snippets"] - assert column.array? + assert_predicate column, :array? end def test_change_column_cant_make_non_array_column_to_array @@ -228,7 +228,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_insert_fixtures tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + assert_deprecated do + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + end assert_equal(PgArray.last.tags, tag_values) end @@ -255,7 +257,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x = PgArray.create!(tags: tags) x.reload - refute x.changed? + assert_not_predicate x, :changed? end def test_quoting_non_standard_delimiters @@ -277,7 +279,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x.reload assert_equal %w(one two three), x.tags - assert_not x.changed? + assert_not_predicate x, :changed? end def test_mutate_value_in_array @@ -288,7 +290,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x.reload assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores - assert_not x.changed? + assert_not_predicate x, :changed? end def test_datetime_with_timezone_awareness @@ -351,7 +353,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert e1.persisted?, "Saving e1" e2 = klass.create("tags" => ["black", "blue"]) - assert !e2.persisted?, "e2 shouldn't be valid" + assert_not e2.persisted?, "e2 shouldn't be valid" assert e2.errors[:tags].any?, "Should have errors for tags" assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags" end diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index df04299569..c8e728bbb6 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -29,20 +29,20 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlBitString.type_for_attribute("a_bit") - assert_not type.binary? + assert_not_predicate type, :binary? 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.array? + assert_not_predicate column, :array? type = PostgresqlBitString.type_for_attribute("a_bit_varying") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index a6bee113ff..64bb6906cd 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -75,7 +75,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_write_value data = "\u001F" record = ByteaDataType.create(payload: data) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_equal(data, record.payload) end @@ -101,14 +101,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_equal(data, record.payload) assert_equal(data, ByteaDataType.where(id: record.id).first.payload) end def test_write_nil record = ByteaDataType.create(payload: nil) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_nil(record.payload) assert_nil(ByteaDataType.where(id: record.id).first.payload) end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index adf461a9cc..6dba4f3e14 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -33,7 +33,7 @@ module ActiveRecord connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp column = connection.columns(:strings).find { |c| c.name == "somedate" } assert_equal :datetime, column.type - assert column.array? + assert_predicate column, :array? end end end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index a25f102bad..9eb0b7d99c 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -32,10 +32,10 @@ class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase column = Citext.columns_hash["cival"] assert_equal :citext, column.type assert_equal "citext", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Citext.type_for_attribute("cival") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_change_table_supports_json diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 5da95f7e2c..b0ce2694a3 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -51,10 +51,10 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlComposite.columns_hash["address"] assert_nil column.type assert_equal "full_address", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_composite_mapping @@ -113,10 +113,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlComposite.columns_hash["address"] assert_equal :full_address, column.type assert_equal "full_address", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_composite_mapping diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 81358b8fc4..54b0dde7dc 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -151,13 +151,13 @@ module ActiveRecord # When prompted, restart the PostgreSQL server with the # "-m fast" option or kill the individual connection assuming # you know the incantation to do that. - # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... + # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ... # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" def test_reconnection_after_actual_disconnection_with_verify original_connection_pid = @connection.query("select pg_backend_pid()") # Sanity check. - assert @connection.active? + assert_predicate @connection, :active? if @connection.send(:postgresql_version) >= 90200 secondary_connection = ActiveRecord::Base.connection_pool.checkout @@ -176,7 +176,7 @@ module ActiveRecord @connection.verify! - assert @connection.active? + assert_predicate @connection, :active? # If we get no exception here, then either we re-connected successfully, or # we never actually got disconnected. diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb new file mode 100644 index 0000000000..a86abac2be --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/date_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase + def test_load_infinity_and_beyond + topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :>, 0 + + topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :<, 0 + end + + def test_save_infinity_and_beyond + topic = Topic.create!(last_read: 1.0 / 0.0) + assert_equal(1.0 / 0.0, topic.last_read) + + topic = Topic.create!(last_read: -1.0 / 0.0) + assert_equal(-1.0 / 0.0, topic.last_read) + end + + def test_bc_date + date = Date.new(0) - 1.week + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_leap_year + date = Time.utc(-4, 2, 29).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_year_zero + date = Time.utc(0, 4, 7).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end +end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index dafbc0a3db..eeaad94c27 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -30,10 +30,10 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlDomain.columns_hash["price"] assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlDomain.type_for_attribute("price") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_domain_acts_like_basetype diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 3d3cbe11a3..6789ff63e7 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -32,10 +32,10 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlEnum.columns_hash["current_mood"] assert_equal :enum, column.type assert_equal "mood", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlEnum.type_for_attribute("current_mood") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_enum_defaults @@ -73,7 +73,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" stderr_output = capture(:stderr) { PostgresqlEnum.first } - assert stderr_output.blank? + assert_predicate stderr_output, :blank? end def test_enum_type_cast diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb new file mode 100644 index 0000000000..4fa315ad23 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/professor" + +if ActiveRecord::Base.connection.supports_foreign_tables? + class ForeignTableTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class ForeignProfessor < ActiveRecord::Base + self.table_name = "foreign_professors" + end + + class ForeignProfessorWithPk < ForeignProfessor + self.primary_key = "id" + end + + def setup + @professor = Professor.create(name: "Nicola") + + @connection = ActiveRecord::Base.connection + enable_extension!("postgres_fdw", @connection) + + foreign_db_config = ARTest.connection_config["arunit2"] + @connection.execute <<-SQL + CREATE SERVER foreign_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '#{foreign_db_config["database"]}') + SQL + + @connection.execute <<-SQL + CREATE USER MAPPING FOR CURRENT_USER + SERVER foreign_server + SQL + + @connection.execute <<-SQL + CREATE FOREIGN TABLE foreign_professors ( + id int, + name character varying NOT NULL + ) SERVER foreign_server OPTIONS ( + table_name 'professors' + ) + SQL + end + + def teardown + disable_extension!("postgres_fdw", @connection) + @connection.execute <<-SQL + DROP SERVER IF EXISTS foreign_server CASCADE + SQL + end + + def test_table_exists + table_name = ForeignProfessor.table_name + assert_not ActiveRecord::Base.connection.table_exists?(table_name) + end + + def test_foreign_tables_are_valid_data_sources + table_name = ForeignProfessor.table_name + assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source" + end + + def test_foreign_tables + assert_equal ["foreign_professors"], @connection.foreign_tables + end + + def test_foreign_table_exists + assert @connection.foreign_table_exists?("foreign_professors") + assert @connection.foreign_table_exists?(:foreign_professors) + assert_not @connection.foreign_table_exists?("nonexistingtable") + assert_not @connection.foreign_table_exists?("'") + assert_not @connection.foreign_table_exists?(nil) + end + + def test_attribute_names + assert_equal ["id", "name"], ForeignProfessor.attribute_names + end + + def test_attributes + professor = ForeignProfessorWithPk.find(@professor.id) + assert_equal @professor.attributes, professor.attributes + end + + def test_does_not_have_a_primary_key + assert_nil ForeignProfessor.primary_key + end + + def test_insert_record + # Explicit `id` here to avoid complex configurations to implicitly work with remote table + ForeignProfessorWithPk.create!(id: 100, name: "Leonardo") + + professor = ForeignProfessorWithPk.last + assert_equal "Leonardo", professor.name + end + + def test_update_record + professor = ForeignProfessorWithPk.find(@professor.id) + professor.name = "Albert" + professor.save! + professor.reload + assert_equal "Albert", professor.name + end + + def test_delete_record + professor = ForeignProfessorWithPk.find(@professor.id) + assert_difference("ForeignProfessor.count", -1) { professor.destroy } + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index c6f1e1727f..95dee3bf44 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -22,10 +22,10 @@ class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase column = Tsvector.columns_hash["text_vector"] assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Tsvector.type_for_attribute("text_vector") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_update_tsvector diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index e1ba00e07b..8c6f046553 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -39,10 +39,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlPoint.type_for_attribute("x") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default @@ -79,7 +79,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase p.reload assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x - assert_not p.changed? + assert_not_predicate p, :changed? end def test_array_assignment @@ -117,10 +117,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlPoint.columns_hash["legacy_x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlPoint.type_for_attribute("legacy_x") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_legacy_default @@ -157,7 +157,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase p.reload assert_equal [10.0, 25.0], p.legacy_x - assert_not p.changed? + assert_not_predicate 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 f09e34b5f2..4b061a9375 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -40,7 +40,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase end def test_hstore_included_in_extensions - assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert_respond_to @connection, :extensions assert_includes @connection.extensions, "hstore", "extension list should include hstore" end @@ -58,9 +58,9 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type - assert_not @column.array? + assert_not_predicate @column, :array? - assert_not @type.binary? + assert_not_predicate @type, :binary? end def test_default @@ -165,7 +165,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase hstore.reload assert_equal "four", hstore.settings["three"] - assert_not hstore.changed? + assert_not_predicate hstore, :changed? end def test_dirty_from_user_equal @@ -174,7 +174,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase hstore.settings = { "key" => "value", "alongkey" => "anything" } assert_equal settings, hstore.settings - refute hstore.changed? + assert_not_predicate hstore, :changed? end def test_hstore_dirty_from_database_equal @@ -184,7 +184,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase assert_equal settings, hstore.settings hstore.settings = settings - refute hstore.changed? + assert_not_predicate hstore, :changed? end def test_gen1 diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 0b18c0c9d7..5e56ce8427 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -13,6 +13,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase @connection.create_table(:postgresql_infinities) do |t| t.float :float t.datetime :datetime + t.date :date end end @@ -43,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase end test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: "infinity") + record.reload + assert_equal Float::INFINITY, record.datetime + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) record.reload assert_equal Float::INFINITY, record.datetime end + test "type casting infinity on a date column" do + record = PostgresqlInfinity.create!(date: "infinity") + record.reload + assert_equal Float::INFINITY, record.date + + record = PostgresqlInfinity.create!(date: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.date + end + test "update_all with infinity on a datetime column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(datetime: Float::INFINITY) @@ -68,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase PostgresqlInfinity.reset_column_information end end + + test "where clause with infinite range on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Time.current) + + string = PostgresqlInfinity.where(datetime: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end + + test "where clause with infinite range on a date column" do + record = PostgresqlInfinity.create!(date: Date.current) + + string = PostgresqlInfinity.where(date: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index eca29f2892..8349ee6ee2 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -31,10 +31,10 @@ class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase column = Ltree.columns_hash["path"] assert_equal :ltree, column.type assert_equal "ltree", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Ltree.type_for_attribute("path") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_write diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index cc10890fa8..61e75e772d 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -6,7 +6,9 @@ require "support/schema_dumping_helper" class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper - class PostgresqlMoney < ActiveRecord::Base; end + class PostgresqlMoney < ActiveRecord::Base + validates :depth, numericality: true + end setup do @connection = ActiveRecord::Base.connection @@ -26,15 +28,16 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase assert_equal :money, column.type assert_equal "money", column.sql_type assert_equal 2, column.scale - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlMoney.type_for_attribute("wealth") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth + assert_equal "$150.55", PostgresqlMoney.new.depth_before_type_cast end def test_money_values diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index f461544a85..736570451b 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -24,30 +24,30 @@ class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlNetworkAddress.columns_hash["cidr_address"] assert_equal :cidr, column.type assert_equal "cidr", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("cidr_address") - assert_not type.binary? + assert_not_predicate type, :binary? 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.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("inet_address") - assert_not type.binary? + assert_not_predicate type, :binary? 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.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("mac_address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_network_types diff --git a/activerecord/test/cases/adapters/postgresql/partitions_test.rb b/activerecord/test/cases/adapters/postgresql/partitions_test.rb new file mode 100644 index 0000000000..0ac9ca1200 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/partitions_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgreSQLPartitionsTest < ActiveRecord::PostgreSQLTestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "partitioned_events", if_exists: true + end + + def test_partitions_table_exists + skip unless ActiveRecord::Base.connection.postgresql_version >= 100000 + @connection.create_table :partitioned_events, force: true, id: false, + options: "partition by range (issued_at)" do |t| + t.timestamp :issued_at + end + assert @connection.table_exists?("partitioned_events") + 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 1951230c8a..cbb6cd42b5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -263,25 +263,25 @@ module ActiveRecord end def test_columns_for_distinct_one_order - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc"]) end def test_columns_for_distinct_few_orders - assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end def test_columns_for_distinct_with_case assert_equal( - "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0", + "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) ) end def test_columns_for_distinct_blank_not_nil_orders - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end @@ -290,23 +290,23 @@ module ActiveRecord def order.to_sql "posts.created_at desc" end - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", [order]) end def test_columns_for_distinct_with_nulls - assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) - assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) + assert_equal "posts.updater_id AS alias_0, posts.title", @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", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id"]) - assert_equal "posts.title, posts.updater_id AS alias_0", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"]) - assert_equal "posts.title, posts.updater_id AS alias_0", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"]) end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 261c24634e..433598500d 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -341,6 +341,12 @@ _SQL assert_equal record, PostgresqlRange.where(int4_range: range).take end + def test_where_by_attribute_with_range_in_array + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: [range]).take + end + def test_update_all_with_ranges PostgresqlRange.create! diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 2c99fa78bd..a36d066c80 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -204,12 +204,12 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path("PUBLIC") do - assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") + assert_not(@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end def test_data_source_exists_wrong_schema - assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") + assert_not(@connection.data_source_exists?("foo.things"), "data_source should not exist") end def test_data_source_exists_quoted_names @@ -532,6 +532,34 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase end end +class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_nulls_order_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "NULLS FIRST" \}/, output) + end + + def test_non_default_order_with_nulls_is_dumped + @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output) + end +end + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 6a99323be5..83ea86be6d 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -24,14 +24,14 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlSerial.columns_hash["seq"] assert_equal :integer, column.type assert_equal "integer", column.sql_type - assert column.serial? + assert_predicate column, :serial? end def test_not_serial_column column = PostgresqlSerial.columns_hash["serials_id"] assert_equal :integer, column.type assert_equal "integer", column.sql_type - assert_not column.serial? + assert_not_predicate column, :serial? end def test_schema_dump_with_shorthand @@ -66,14 +66,14 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlBigSerial.columns_hash["seq"] assert_equal :integer, column.type assert_equal "bigint", column.sql_type - assert column.serial? + assert_predicate column, :serial? end def test_not_bigserial_column column = PostgresqlBigSerial.columns_hash["serials_id"] assert_equal :integer, column.type assert_equal "bigint", column.sql_type - assert_not column.serial? + assert_not_predicate column, :serial? end def test_schema_dump_with_shorthand @@ -111,7 +111,7 @@ module SequenceNameDetectionTestCases columns = @connection.columns(:foo) columns.each do |column| assert_equal :integer, column.type - assert column.serial? + assert_predicate column, :serial? end end @@ -142,7 +142,7 @@ module SequenceNameDetectionTestCases columns = @connection.columns(@table_name) columns.each do |column| assert_equal :integer, column.type - assert column.serial? + assert_predicate column, :serial? end end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 9821b103df..984b2f5ea4 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -14,7 +14,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false - Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception @connection = ActiveRecord::Base.connection @@ -32,7 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort - Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = @original_report_on_exception end test "raises SerializationFailure when a serialization failure occurs" do @@ -76,7 +76,7 @@ module ActiveRecord Sample.transaction do s1.lock! barrier.wait - s2.update_attributes value: 1 + s2.update value: 1 end end @@ -84,7 +84,7 @@ module ActiveRecord Sample.transaction do s2.lock! barrier.wait - s1.update_attributes value: 2 + s1.update value: 2 end ensure thread.join diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index c24e0cb330..71d07e2f4c 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -82,7 +82,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] - assert column.array? + assert_predicate column, :array? assert_equal "{}", column.default schema = dump_table_schema "uuid_data_type" @@ -93,10 +93,10 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type assert_equal "uuid", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = UUIDType.type_for_attribute("guid") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_treat_blank_uuid_as_nil @@ -178,7 +178,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase duplicate = klass.new(guid: record.guid) assert record.guid.present? # Ensure we actually are testing a UUID - assert_not duplicate.valid? + assert_not_predicate duplicate, :valid? end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 6fdb353368..40b58e86bf 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -55,4 +55,37 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) end + + def test_quoted_time_normalizes_date_qualified_time + value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999) + type = ActiveRecord::Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) + end + + def test_quoted_time_dst_utc + with_env_tz "America/New_York" do + with_timezone_config default: :utc do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getutc.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") + + assert_equal expected, @conn.quoted_time(t) + end + end + end + + def test_quoted_time_dst_local + with_env_tz "America/New_York" do + with_timezone_config default: :local do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") + + assert_equal expected, @conn.quoted_time(t) + 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 20579c6476..d1d4d545a3 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -61,7 +61,7 @@ module ActiveRecord WHERE #{Owner.primary_key} = #{owner.id} esql - assert(!result.rows.first.include?("blob"), "should not store blobs") + assert_not(result.rows.first.include?("blob"), "should not store blobs") ensure owner.delete end @@ -475,11 +475,11 @@ module ActiveRecord end def test_respond_to_enable_extension - assert @conn.respond_to?(:enable_extension) + assert_respond_to @conn, :enable_extension end def test_respond_to_disable_extension - assert @conn.respond_to?(:disable_extension) + assert_respond_to @conn, :disable_extension end def test_statement_closed @@ -504,6 +504,39 @@ module ActiveRecord assert_deprecated { @conn.valid_alter_table_type?(:string) } end + def test_db_is_not_readonly_when_readonly_option_is_false + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: false + + assert_not_predicate conn.raw_connection, :readonly? + end + + def test_db_is_not_readonly_when_readonly_option_is_unspecified + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3" + + assert_not_predicate conn.raw_connection, :readonly? + end + + def test_db_is_readonly_when_readonly_option_is_true + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: true + + assert_predicate conn.raw_connection, :readonly? + end + + def test_writes_are_not_permitted_to_readonly_databases + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: true + + assert_raises(ActiveRecord::StatementInvalid, /SQLite3::ReadOnlyException/) do + conn.execute("CREATE TABLE test(id integer)") + end + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 83974f327e..f05dcac7dd 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -48,7 +48,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version end def test_schema_define_w_table_name_prefix @@ -64,7 +64,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase t.column :flavor, :string end end - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version ensure ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name @@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_change_table @@ -129,8 +129,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_add_timestamps @@ -139,7 +139,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase add_timestamps :has_timestamps, default: Time.now end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb new file mode 100644 index 0000000000..671e273543 --- /dev/null +++ b/activerecord/test/cases/arel/attributes/attribute_test.rb @@ -0,0 +1,1015 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "ostruct" + +module Arel + module Attributes + class AttributeTest < Arel::Spec + describe "#not_eq" do + it "should create a NotEqual node" do + relation = Table.new(:users) + relation[:id].not_eq(10).must_be_kind_of Nodes::NotEqual + end + + it "should generate != in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_eq(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" != 10 + } + end + + it "should handle nil" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_eq(nil) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL + } + end + end + + describe "#not_eq_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].not_eq_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_eq_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2) + } + end + end + + describe "#not_eq_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].not_eq_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_eq_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2) + } + end + end + + describe "#gt" do + it "should create a GreaterThan node" do + relation = Table.new(:users) + relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan + end + + it "should generate > in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gt(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" > 10 + } + end + + it "should handle comparing with a subquery" do + users = Table.new(:users) + + avg = users.project(users[:karma].average) + mgr = users.project(Arel.star).where(users[:karma].gt(avg)) + + mgr.to_sql.must_be_like %{ + SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") FROM "users") + } + end + + it "should accept various data types." do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].gt("fake_name") + mgr.to_sql.must_match %{"users"."name" > 'fake_name'} + + current_time = ::Time.now + mgr.where relation[:created_at].gt(current_time) + mgr.to_sql.must_match %{"users"."created_at" > '#{current_time}'} + end + end + + describe "#gt_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].gt_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gt_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 OR "users"."id" > 2) + } + end + end + + describe "#gt_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].gt_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gt_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 AND "users"."id" > 2) + } + end + end + + describe "#gteq" do + it "should create a GreaterThanOrEqual node" do + relation = Table.new(:users) + relation[:id].gteq(10).must_be_kind_of Nodes::GreaterThanOrEqual + end + + it "should generate >= in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gteq(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10 + } + end + + it "should accept various data types." do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].gteq("fake_name") + mgr.to_sql.must_match %{"users"."name" >= 'fake_name'} + + current_time = ::Time.now + mgr.where relation[:created_at].gteq(current_time) + mgr.to_sql.must_match %{"users"."created_at" >= '#{current_time}'} + end + end + + describe "#gteq_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].gteq_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gteq_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 OR "users"."id" >= 2) + } + end + end + + describe "#gteq_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].gteq_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].gteq_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" >= 2) + } + end + end + + describe "#lt" do + it "should create a LessThan node" do + relation = Table.new(:users) + relation[:id].lt(10).must_be_kind_of Nodes::LessThan + end + + it "should generate < in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lt(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" < 10 + } + end + + it "should accept various data types." do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].lt("fake_name") + mgr.to_sql.must_match %{"users"."name" < 'fake_name'} + + current_time = ::Time.now + mgr.where relation[:created_at].lt(current_time) + mgr.to_sql.must_match %{"users"."created_at" < '#{current_time}'} + end + end + + describe "#lt_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].lt_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lt_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 OR "users"."id" < 2) + } + end + end + + describe "#lt_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].lt_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lt_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 AND "users"."id" < 2) + } + end + end + + describe "#lteq" do + it "should create a LessThanOrEqual node" do + relation = Table.new(:users) + relation[:id].lteq(10).must_be_kind_of Nodes::LessThanOrEqual + end + + it "should generate <= in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lteq(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10 + } + end + + it "should accept various data types." do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].lteq("fake_name") + mgr.to_sql.must_match %{"users"."name" <= 'fake_name'} + + current_time = ::Time.now + mgr.where relation[:created_at].lteq(current_time) + mgr.to_sql.must_match %{"users"."created_at" <= '#{current_time}'} + end + end + + describe "#lteq_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].lteq_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lteq_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 OR "users"."id" <= 2) + } + end + end + + describe "#lteq_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].lteq_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].lteq_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 AND "users"."id" <= 2) + } + end + end + + describe "#average" do + it "should create a AVG node" do + relation = Table.new(:users) + relation[:id].average.must_be_kind_of Nodes::Avg + end + + it "should generate the proper SQL" do + relation = Table.new(:users) + mgr = relation.project relation[:id].average + mgr.to_sql.must_be_like %{ + SELECT AVG("users"."id") + FROM "users" + } + end + end + + describe "#maximum" do + it "should create a MAX node" do + relation = Table.new(:users) + relation[:id].maximum.must_be_kind_of Nodes::Max + end + + it "should generate proper SQL" do + relation = Table.new(:users) + mgr = relation.project relation[:id].maximum + mgr.to_sql.must_be_like %{ + SELECT MAX("users"."id") + FROM "users" + } + end + end + + describe "#minimum" do + it "should create a Min node" do + relation = Table.new(:users) + relation[:id].minimum.must_be_kind_of Nodes::Min + end + + it "should generate proper SQL" do + relation = Table.new(:users) + mgr = relation.project relation[:id].minimum + mgr.to_sql.must_be_like %{ + SELECT MIN("users"."id") + FROM "users" + } + end + end + + describe "#sum" do + it "should create a SUM node" do + relation = Table.new(:users) + relation[:id].sum.must_be_kind_of Nodes::Sum + end + + it "should generate the proper SQL" do + relation = Table.new(:users) + mgr = relation.project relation[:id].sum + mgr.to_sql.must_be_like %{ + SELECT SUM("users"."id") + FROM "users" + } + end + end + + describe "#count" do + it "should return a count node" do + relation = Table.new(:users) + relation[:id].count.must_be_kind_of Nodes::Count + end + + it "should take a distinct param" do + relation = Table.new(:users) + count = relation[:id].count(nil) + count.must_be_kind_of Nodes::Count + count.distinct.must_be_nil + end + end + + describe "#eq" do + it "should return an equality node" do + attribute = Attribute.new nil, nil + equality = attribute.eq 1 + equality.left.must_equal attribute + equality.right.val.must_equal 1 + equality.must_be_kind_of Nodes::Equality + end + + it "should generate = in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].eq(10) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" = 10 + } + end + + it "should handle nil" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].eq(nil) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL + } + end + end + + describe "#eq_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].eq_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].eq_any([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" = 2) + } + end + + it "should not eat input" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + values = [1, 2] + mgr.where relation[:id].eq_any(values) + values.must_equal [1, 2] + end + end + + describe "#eq_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].eq_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2) + } + end + + it "should not eat input" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + values = [1, 2] + mgr.where relation[:id].eq_all(values) + values.must_equal [1, 2] + end + end + + describe "#matches" do + it "should create a Matches node" do + relation = Table.new(:users) + relation[:name].matches("%bacon%").must_be_kind_of Nodes::Matches + end + + it "should generate LIKE in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].matches("%bacon%") + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%' + } + end + end + + describe "#matches_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:name].matches_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].matches_any(["%chunky%", "%bacon%"]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%') + } + end + end + + describe "#matches_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:name].matches_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].matches_all(["%chunky%", "%bacon%"]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%') + } + end + end + + describe "#does_not_match" do + it "should create a DoesNotMatch node" do + relation = Table.new(:users) + relation[:name].does_not_match("%bacon%").must_be_kind_of Nodes::DoesNotMatch + end + + it "should generate NOT LIKE in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].does_not_match("%bacon%") + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%' + } + end + end + + describe "#does_not_match_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:name].does_not_match_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].does_not_match_any(["%chunky%", "%bacon%"]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" NOT LIKE '%bacon%') + } + end + end + + describe "#does_not_match_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:name].does_not_match_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" NOT LIKE '%bacon%') + } + end + end + + describe "with a range" do + it "can be constructed with a standard range" do + attribute = Attribute.new nil, nil + node = attribute.between(1..3) + + node.must_equal Nodes::Between.new( + attribute, + Nodes::And.new([ + Nodes::Casted.new(1, attribute), + Nodes::Casted.new(3, attribute) + ]) + ) + end + + it "can be constructed with a range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(-::Float::INFINITY..3) + + node.must_equal Nodes::LessThanOrEqual.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + end + + it "can be constructed with a quoted range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(quoted_range(-::Float::INFINITY, 3, false)) + + node.must_equal Nodes::LessThanOrEqual.new( + attribute, + Nodes::Quoted.new(3) + ) + end + + it "can be constructed with an exclusive range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(-::Float::INFINITY...3) + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + end + + it "can be constructed with a quoted exclusive range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(quoted_range(-::Float::INFINITY, 3, true)) + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Quoted.new(3) + ) + end + + it "can be constructed with an infinite range" do + attribute = Attribute.new nil, nil + node = attribute.between(-::Float::INFINITY..::Float::INFINITY) + + node.must_equal Nodes::NotIn.new(attribute, []) + end + + it "can be constructed with a quoted infinite range" do + attribute = Attribute.new nil, nil + node = attribute.between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false)) + + node.must_equal Nodes::NotIn.new(attribute, []) + end + + + it "can be constructed with a range ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(0..::Float::INFINITY) + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + + it "can be constructed with a quoted range ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(quoted_range(0, ::Float::INFINITY, false)) + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Quoted.new(0) + ) + end + + it "can be constructed with an exclusive range" do + attribute = Attribute.new nil, nil + node = attribute.between(0...3) + + node.must_equal Nodes::And.new([ + Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ), + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + ]) + end + + def quoted_range(begin_val, end_val, exclude) + OpenStruct.new( + begin: Nodes::Quoted.new(begin_val), + end: Nodes::Quoted.new(end_val), + exclude_end?: exclude, + ) + end + end + + describe "#in" do + it "can be constructed with a subquery" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"]) + attribute = Attribute.new nil, nil + + node = attribute.in(mgr) + + node.must_equal Nodes::In.new(attribute, mgr.ast) + end + + it "can be constructed with a list" do + attribute = Attribute.new nil, nil + node = attribute.in([1, 2, 3]) + + node.must_equal Nodes::In.new( + attribute, + [ + Nodes::Casted.new(1, attribute), + Nodes::Casted.new(2, attribute), + Nodes::Casted.new(3, attribute), + ] + ) + end + + it "can be constructed with a random object" do + attribute = Attribute.new nil, nil + random_object = Object.new + node = attribute.in(random_object) + + node.must_equal Nodes::In.new( + attribute, + Nodes::Casted.new(random_object, attribute) + ) + end + + it "should generate IN in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].in([1, 2, 3]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" IN (1, 2, 3) + } + end + end + + describe "#in_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].in_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].in_any([[1, 2], [3, 4]]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."id" IN (3, 4)) + } + end + end + + describe "#in_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].in_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].in_all([[1, 2], [3, 4]]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) AND "users"."id" IN (3, 4)) + } + end + end + + describe "with a range" do + it "can be constructed with a standard range" do + attribute = Attribute.new nil, nil + node = attribute.not_between(1..3) + + node.must_equal Nodes::Grouping.new(Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(1, attribute) + ), + Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + )) + end + + it "can be constructed with a range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(-::Float::INFINITY..3) + + node.must_equal Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + end + + it "can be constructed with an exclusive range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(-::Float::INFINITY...3) + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + end + + it "can be constructed with an infinite range" do + attribute = Attribute.new nil, nil + node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY) + + node.must_equal Nodes::In.new(attribute, []) + end + + it "can be constructed with a range ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(0..::Float::INFINITY) + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + + it "can be constructed with an exclusive range" do + attribute = Attribute.new nil, nil + node = attribute.not_between(0...3) + + node.must_equal Nodes::Grouping.new(Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ), + Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(3, attribute) + ) + )) + end + end + + describe "#not_in" do + it "can be constructed with a subquery" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"]) + attribute = Attribute.new nil, nil + + node = attribute.not_in(mgr) + + node.must_equal Nodes::NotIn.new(attribute, mgr.ast) + end + + it "can be constructed with a Union" do + relation = Table.new(:users) + mgr1 = relation.project(relation[:id]) + mgr2 = relation.project(relation[:id]) + + union = mgr1.union(mgr2) + node = relation[:id].in(union) + node.to_sql.must_be_like %{ + "users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" )) + } + end + + it "can be constructed with a list" do + attribute = Attribute.new nil, nil + node = attribute.not_in([1, 2, 3]) + + node.must_equal Nodes::NotIn.new( + attribute, + [ + Nodes::Casted.new(1, attribute), + Nodes::Casted.new(2, attribute), + Nodes::Casted.new(3, attribute), + ] + ) + end + + it "can be constructed with a random object" do + attribute = Attribute.new nil, nil + random_object = Object.new + node = attribute.not_in(random_object) + + node.must_equal Nodes::NotIn.new( + attribute, + Nodes::Casted.new(random_object, attribute) + ) + end + + it "should generate NOT IN in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_in([1, 2, 3]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE "users"."id" NOT IN (1, 2, 3) + } + end + end + + describe "#not_in_any" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].not_in_any([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ORs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_in_any([[1, 2], [3, 4]]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) OR "users"."id" NOT IN (3, 4)) + } + end + end + + describe "#not_in_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].not_in_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].not_in_all([[1, 2], [3, 4]]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) AND "users"."id" NOT IN (3, 4)) + } + end + end + + describe "#eq_all" do + it "should create a Grouping node" do + relation = Table.new(:users) + relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping + end + + it "should generate ANDs in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.where relation[:id].eq_all([1, 2]) + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2) + } + end + end + + describe "#asc" do + it "should create an Ascending node" do + relation = Table.new(:users) + relation[:id].asc.must_be_kind_of Nodes::Ascending + end + + it "should generate ASC in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.order relation[:id].asc + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC + } + end + end + + describe "#desc" do + it "should create a Descending node" do + relation = Table.new(:users) + relation[:id].desc.must_be_kind_of Nodes::Descending + end + + it "should generate DESC in sql" do + relation = Table.new(:users) + mgr = relation.project relation[:id] + mgr.order relation[:id].desc + mgr.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC + } + end + end + + describe "equality" do + describe "#to_sql" do + it "should produce sql" do + table = Table.new :users + condition = table["id"].eq 1 + condition.to_sql.must_equal '"users"."id" = 1' + end + end + end + + describe "type casting" do + it "does not type cast by default" do + table = Table.new(:foo) + condition = table["id"].eq("1") + + assert_not table.able_to_type_cast? + condition.to_sql.must_equal %("foo"."id" = '1') + end + + it "type casts when given an explicit caster" do + fake_caster = Object.new + def fake_caster.type_cast_for_database(attr_name, value) + if attr_name == "id" + value.to_i + else + value + end + end + table = Table.new(:foo, type_caster: fake_caster) + condition = table["id"].eq("1").and(table["other_id"].eq("2")) + + assert table.able_to_type_cast? + condition.to_sql.must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2') + end + + it "does not type cast SqlLiteral nodes" do + fake_caster = Object.new + def fake_caster.type_cast_for_database(attr_name, value) + value.to_i + end + table = Table.new(:foo, type_caster: fake_caster) + condition = table["id"].eq(Arel.sql("(select 1)")) + + assert table.able_to_type_cast? + condition.to_sql.must_equal %("foo"."id" = (select 1)) + end + end + end + end +end diff --git a/activerecord/test/cases/arel/attributes/math_test.rb b/activerecord/test/cases/arel/attributes/math_test.rb new file mode 100644 index 0000000000..41eea217c0 --- /dev/null +++ b/activerecord/test/cases/arel/attributes/math_test.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Attributes + class MathTest < Arel::Spec + %i[* /].each do |math_operator| + it "average should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{ + AVG("users"."id") #{math_operator} 2 + } + end + + it "count should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{ + COUNT("users"."id") #{math_operator} 2 + } + end + + it "maximum should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{ + MAX("users"."id") #{math_operator} 2 + } + end + + it "minimum should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{ + MIN("users"."id") #{math_operator} 2 + } + end + + it "attribute node should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{ + "users"."id" #{math_operator} 2 + } + end + end + + %i[+ - & | ^ << >>].each do |math_operator| + it "average should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{ + (AVG("users"."id") #{math_operator} 2) + } + end + + it "count should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{ + (COUNT("users"."id") #{math_operator} 2) + } + end + + it "maximum should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{ + (MAX("users"."id") #{math_operator} 2) + } + end + + it "minimum should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{ + (MIN("users"."id") #{math_operator} 2) + } + end + + it "attribute node should be compatible with #{math_operator}" do + table = Arel::Table.new :users + (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{ + ("users"."id" #{math_operator} 2) + } + end + end + end + end +end diff --git a/activerecord/test/cases/arel/attributes_test.rb b/activerecord/test/cases/arel/attributes_test.rb new file mode 100644 index 0000000000..b00af4bd29 --- /dev/null +++ b/activerecord/test/cases/arel/attributes_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + describe "Attributes" do + it "responds to lower" do + relation = Table.new(:users) + attribute = relation[:foo] + node = attribute.lower + assert_equal "LOWER", node.name + assert_equal [attribute], node.expressions + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Attribute.new("foo", "bar"), Attribute.new("foo", "bar")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Attribute.new("foo", "bar"), Attribute.new("foo", "baz")] + assert_equal 2, array.uniq.size + end + end + + describe "for" do + it "deals with unknown column types" do + column = Struct.new(:type).new :crazy + Attributes.for(column).must_equal Attributes::Undefined + end + + it "returns the correct constant for strings" do + [:string, :text, :binary].each do |type| + column = Struct.new(:type).new type + Attributes.for(column).must_equal Attributes::String + end + end + + it "returns the correct constant for ints" do + column = Struct.new(:type).new :integer + Attributes.for(column).must_equal Attributes::Integer + end + + it "returns the correct constant for floats" do + column = Struct.new(:type).new :float + Attributes.for(column).must_equal Attributes::Float + end + + it "returns the correct constant for decimals" do + column = Struct.new(:type).new :decimal + Attributes.for(column).must_equal Attributes::Decimal + end + + it "returns the correct constant for boolean" do + column = Struct.new(:type).new :boolean + Attributes.for(column).must_equal Attributes::Boolean + end + + it "returns the correct constant for time" do + [:date, :datetime, :timestamp, :time].each do |type| + column = Struct.new(:type).new type + Attributes.for(column).must_equal Attributes::Time + end + end + end + end +end diff --git a/activerecord/test/cases/arel/collectors/bind_test.rb b/activerecord/test/cases/arel/collectors/bind_test.rb new file mode 100644 index 0000000000..ffa9b15f66 --- /dev/null +++ b/activerecord/test/cases/arel/collectors/bind_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "arel/collectors/bind" + +module Arel + module Collectors + class TestBind < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def collect(node) + @visitor.accept(node, Collectors::Bind.new) + end + + def compile(node) + collect(node).value + end + + def ast_with_binds(bvs) + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift))) + manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift))) + manager.ast + end + + def test_compile_gathers_all_bind_params + binds = compile(ast_with_binds(["hello", "world"])) + assert_equal ["hello", "world"], binds + + binds = compile(ast_with_binds(["hello2", "world3"])) + assert_equal ["hello2", "world3"], binds + end + end + end +end diff --git a/activerecord/test/cases/arel/collectors/composite_test.rb b/activerecord/test/cases/arel/collectors/composite_test.rb new file mode 100644 index 0000000000..545637496f --- /dev/null +++ b/activerecord/test/cases/arel/collectors/composite_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "../helper" + +require "arel/collectors/bind" +require "arel/collectors/composite" + +module Arel + module Collectors + class TestComposite < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def collect(node) + sql_collector = Collectors::SQLString.new + bind_collector = Collectors::Bind.new + collector = Collectors::Composite.new(sql_collector, bind_collector) + @visitor.accept(node, collector) + end + + def compile(node) + collect(node).value + end + + def ast_with_binds(bvs) + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift))) + manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift))) + manager.ast + end + + def test_composite_collector_performs_multiple_collections_at_once + sql, binds = compile(ast_with_binds(["hello", "world"])) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql + assert_equal ["hello", "world"], binds + + sql, binds = compile(ast_with_binds(["hello2", "world3"])) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql + assert_equal ["hello2", "world3"], binds + end + end + end +end diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb new file mode 100644 index 0000000000..380573494d --- /dev/null +++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Collectors + class TestSqlString < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def collect(node) + @visitor.accept(node, Collectors::SQLString.new) + end + + def compile(node) + collect(node).value + end + + def ast_with_binds(bv) + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(bv)) + manager.where(table[:name].eq(bv)) + manager.ast + end + + def test_compile + bv = Nodes::BindParam.new(1) + collector = collect ast_with_binds bv + + sql = collector.compile ["hello", "world"] + assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql + end + + def test_returned_sql_uses_utf8_encoding + bv = Nodes::BindParam.new(1) + collector = collect ast_with_binds bv + + sql = collector.compile ["hello", "world"] + assert_equal sql.encoding, Encoding::UTF_8 + end + end + end +end diff --git a/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb new file mode 100644 index 0000000000..255c8e79e9 --- /dev/null +++ b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "arel/collectors/substitute_binds" +require "arel/collectors/sql_string" + +module Arel + module Collectors + class TestSubstituteBindCollector < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def ast_with_binds + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new("hello"))) + manager.where(table[:name].eq(Nodes::BindParam.new("world"))) + manager.ast + end + + def compile(node, quoter) + collector = Collectors::SubstituteBinds.new(quoter, Collectors::SQLString.new) + @visitor.accept(node, collector).value + end + + def test_compile + quoter = Object.new + def quoter.quote(val) + val.to_s + end + sql = compile(ast_with_binds, quoter) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql + end + + def test_quoting_is_delegated_to_quoter + quoter = Object.new + def quoter.quote(val) + val.inspect + end + sql = compile(ast_with_binds, quoter) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = "hello" AND "users"."name" = "world"', sql + end + end + end +end diff --git a/activerecord/test/cases/arel/crud_test.rb b/activerecord/test/cases/arel/crud_test.rb new file mode 100644 index 0000000000..f3cdd8927f --- /dev/null +++ b/activerecord/test/cases/arel/crud_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class FakeCrudder < SelectManager + class FakeEngine + attr_reader :calls, :connection_pool, :spec, :config + + def initialize + @calls = [] + @connection_pool = self + @spec = self + @config = { adapter: "sqlite3" } + end + + def connection; self end + + def method_missing(name, *args) + @calls << [name, args] + end + end + + include Crud + + attr_reader :engine + attr_accessor :ctx + + def initialize(engine = FakeEngine.new) + super + end + end + + describe "crud" do + describe "insert" do + it "should call insert on the connection" do + table = Table.new :users + fc = FakeCrudder.new + fc.from table + im = fc.compile_insert [[table[:id], "foo"]] + assert_instance_of Arel::InsertManager, im + end + end + + describe "update" do + it "should call update on the connection" do + table = Table.new :users + fc = FakeCrudder.new + fc.from table + stmt = fc.compile_update [[table[:id], "foo"]], Arel::Attributes::Attribute.new(table, "id") + assert_instance_of Arel::UpdateManager, stmt + end + end + + describe "delete" do + it "should call delete on the connection" do + table = Table.new :users + fc = FakeCrudder.new + fc.from table + stmt = fc.compile_delete + assert_instance_of Arel::DeleteManager, stmt + end + end + end +end diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb new file mode 100644 index 0000000000..17a5271039 --- /dev/null +++ b/activerecord/test/cases/arel/delete_manager_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class DeleteManagerTest < Arel::Spec + describe "new" do + it "takes an engine" do + Arel::DeleteManager.new + end + end + + it "handles limit properly" do + table = Table.new(:users) + dm = Arel::DeleteManager.new + dm.take 10 + dm.from table + assert_match(/LIMIT 10/, dm.to_sql) + end + + describe "from" do + it "uses from" do + table = Table.new(:users) + dm = Arel::DeleteManager.new + dm.from table + dm.to_sql.must_be_like %{ DELETE FROM "users" } + end + + it "chains" do + table = Table.new(:users) + dm = Arel::DeleteManager.new + dm.from(table).must_equal dm + end + end + + describe "where" do + it "uses where values" do + table = Table.new(:users) + dm = Arel::DeleteManager.new + dm.from table + dm.where table[:id].eq(10) + dm.to_sql.must_be_like %{ DELETE FROM "users" WHERE "users"."id" = 10} + end + + it "chains" do + table = Table.new(:users) + dm = Arel::DeleteManager.new + dm.where(table[:id].eq(10)).must_equal dm + end + end + end +end diff --git a/activerecord/test/cases/arel/factory_methods_test.rb b/activerecord/test/cases/arel/factory_methods_test.rb new file mode 100644 index 0000000000..26d2cdd08d --- /dev/null +++ b/activerecord/test/cases/arel/factory_methods_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + module FactoryMethods + class TestFactoryMethods < Arel::Test + class Factory + include Arel::FactoryMethods + end + + def setup + @factory = Factory.new + end + + def test_create_join + join = @factory.create_join :one, :two + assert_kind_of Nodes::Join, join + assert_equal :two, join.right + end + + def test_create_on + on = @factory.create_on :one + assert_instance_of Nodes::On, on + assert_equal :one, on.expr + end + + def test_create_true + true_node = @factory.create_true + assert_instance_of Nodes::True, true_node + end + + def test_create_false + false_node = @factory.create_false + assert_instance_of Nodes::False, false_node + end + + def test_lower + lower = @factory.lower :one + assert_instance_of Nodes::NamedFunction, lower + assert_equal "LOWER", lower.name + assert_equal [:one], lower.expressions.map(&:expr) + end + end + end +end diff --git a/activerecord/test/cases/arel/helper.rb b/activerecord/test/cases/arel/helper.rb new file mode 100644 index 0000000000..f8ce658440 --- /dev/null +++ b/activerecord/test/cases/arel/helper.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "active_support" +require "minitest/autorun" +require "arel" + +require_relative "support/fake_record" + +class Object + def must_be_like(other) + gsub(/\s+/, " ").strip.must_equal other.gsub(/\s+/, " ").strip + end +end + +module Arel + class Test < ActiveSupport::TestCase + def setup + super + @arel_engine = Arel::Table.engine + Arel::Table.engine = FakeRecord::Base.new + end + + def teardown + Arel::Table.engine = @arel_engine if defined? @arel_engine + super + end + end + + class Spec < Minitest::Spec + before do + @arel_engine = Arel::Table.engine + Arel::Table.engine = FakeRecord::Base.new + end + + after do + Arel::Table.engine = @arel_engine if defined? @arel_engine + end + include ActiveSupport::Testing::Assertions + + # test/unit backwards compatibility methods + alias :assert_no_match :refute_match + alias :assert_not_equal :refute_equal + alias :assert_not_same :refute_same + end +end diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb new file mode 100644 index 0000000000..2376ad8d37 --- /dev/null +++ b/activerecord/test/cases/arel/insert_manager_test.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class InsertManagerTest < Arel::Spec + describe "new" do + it "takes an engine" do + Arel::InsertManager.new + end + end + + describe "insert" do + it "can create a Values node" do + manager = Arel::InsertManager.new + values = manager.create_values %w{ a b }, %w{ c d } + + assert_kind_of Arel::Nodes::Values, values + assert_equal %w{ a b }, values.left + assert_equal %w{ c d }, values.right + end + + it "allows sql literals" do + manager = Arel::InsertManager.new + manager.into Table.new(:users) + manager.values = manager.create_values [Arel.sql("*")], %w{ a } + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" VALUES (*) + } + end + + it "works with multiple values" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:id] + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + %w{1 david}, + %w{2 kir}, + ["3", Arel.sql("DEFAULT")], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"id\", \"name\") VALUES ('1', 'david'), ('2', 'kir'), ('3', DEFAULT) + } + end + + it "literals in multiple values are not escaped" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + [Arel.sql("*")], + [Arel.sql("DEFAULT")], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"name\") VALUES (*), (DEFAULT) + } + end + + it "works with multiple single values" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + %w{david}, + %w{kir}, + [Arel.sql("DEFAULT")], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"name\") VALUES ('david'), ('kir'), (DEFAULT) + } + end + + it "inserts false" do + table = Table.new(:users) + manager = Arel::InsertManager.new + + manager.insert [[table[:bool], false]] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("bool") VALUES ('f') + } + end + + it "inserts null" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.insert [[table[:id], nil]] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id") VALUES (NULL) + } + end + + it "inserts time" do + table = Table.new(:users) + manager = Arel::InsertManager.new + + time = Time.now + attribute = table[:created_at] + + manager.insert [[attribute, time]] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("created_at") VALUES (#{Table.engine.connection.quote time}) + } + end + + it "takes a list of lists" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + manager.insert [[table[:id], 1], [table[:name], "aaron"]] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id", "name") VALUES (1, 'aaron') + } + end + + it "defaults the table" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.insert [[table[:id], 1], [table[:name], "aaron"]] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id", "name") VALUES (1, 'aaron') + } + end + + it "noop for empty list" do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.insert [[table[:id], 1]] + manager.insert [] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id") VALUES (1) + } + end + + it "is chainable" do + table = Table.new(:users) + manager = Arel::InsertManager.new + insert_result = manager.insert [[table[:id], 1]] + assert_equal manager, insert_result + end + end + + describe "into" do + it "takes a Table and chains" do + manager = Arel::InsertManager.new + manager.into(Table.new(:users)).must_equal manager + end + + it "converts to sql" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + manager.to_sql.must_be_like %{ + INSERT INTO "users" + } + end + end + + describe "columns" do + it "converts to sql" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + manager.columns << table[:id] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id") + } + end + end + + describe "values" do + it "converts to sql" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + + manager.values = Nodes::Values.new [1] + manager.to_sql.must_be_like %{ + INSERT INTO "users" VALUES (1) + } + end + + it "accepts sql literals" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + + manager.values = Arel.sql("DEFAULT VALUES") + manager.to_sql.must_be_like %{ + INSERT INTO "users" DEFAULT VALUES + } + end + end + + describe "combo" do + it "combines columns and values list in order" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + + manager.values = Nodes::Values.new [1, "aaron"] + manager.columns << table[:id] + manager.columns << table[:name] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id", "name") VALUES (1, 'aaron') + } + end + end + + describe "select" do + it "accepts a select query in place of a VALUES clause" do + table = Table.new :users + + manager = Arel::InsertManager.new + manager.into table + + select = Arel::SelectManager.new + select.project Arel.sql("1") + select.project Arel.sql('"aaron"') + + manager.select select + manager.columns << table[:id] + manager.columns << table[:name] + manager.to_sql.must_be_like %{ + INSERT INTO "users" ("id", "name") (SELECT 1, "aaron") + } + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb new file mode 100644 index 0000000000..eff54abd91 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/and_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "And" do + describe "equality" do + it "is equal with equal ivars" do + array = [And.new(["foo", "bar"]), And.new(["foo", "bar"])] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [And.new(["foo", "bar"]), And.new(["foo", "baz"])] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/as_test.rb b/activerecord/test/cases/arel/nodes/as_test.rb new file mode 100644 index 0000000000..1169ea11c9 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/as_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "As" do + describe "#as" do + it "makes an AS node" do + attr = Table.new(:users)[:id] + as = attr.as(Arel.sql("foo")) + assert_equal attr, as.left + assert_equal "foo", as.right + end + + it "converts right to SqlLiteral if a string" do + attr = Table.new(:users)[:id] + as = attr.as("foo") + assert_kind_of Arel::Nodes::SqlLiteral, as.right + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [As.new("foo", "bar"), As.new("foo", "bar")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [As.new("foo", "bar"), As.new("foo", "baz")] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/ascending_test.rb b/activerecord/test/cases/arel/nodes/ascending_test.rb new file mode 100644 index 0000000000..4811e6ff5b --- /dev/null +++ b/activerecord/test/cases/arel/nodes/ascending_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestAscending < Arel::Test + def test_construct + ascending = Ascending.new "zomg" + assert_equal "zomg", ascending.expr + end + + def test_reverse + ascending = Ascending.new "zomg" + descending = ascending.reverse + assert_kind_of Descending, descending + assert_equal ascending.expr, descending.expr + end + + def test_direction + ascending = Ascending.new "zomg" + assert_equal :asc, ascending.direction + end + + def test_ascending? + ascending = Ascending.new "zomg" + assert ascending.ascending? + end + + def test_descending? + ascending = Ascending.new "zomg" + assert_not ascending.descending? + end + + def test_equality_with_same_ivars + array = [Ascending.new("zomg"), Ascending.new("zomg")] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [Ascending.new("zomg"), Ascending.new("zomg!")] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/bin_test.rb b/activerecord/test/cases/arel/nodes/bin_test.rb new file mode 100644 index 0000000000..ee2ec3cf2f --- /dev/null +++ b/activerecord/test/cases/arel/nodes/bin_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestBin < Arel::Test + def test_new + assert Arel::Nodes::Bin.new("zomg") + end + + def test_default_to_sql + viz = Arel::Visitors::ToSql.new Table.engine.connection_pool + node = Arel::Nodes::Bin.new(Arel.sql("zomg")) + assert_equal "zomg", viz.accept(node, Collectors::SQLString.new).value + end + + def test_mysql_to_sql + viz = Arel::Visitors::MySQL.new Table.engine.connection_pool + node = Arel::Nodes::Bin.new(Arel.sql("zomg")) + assert_equal "BINARY zomg", viz.accept(node, Collectors::SQLString.new).value + end + + def test_equality_with_same_ivars + array = [Bin.new("zomg"), Bin.new("zomg")] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [Bin.new("zomg"), Bin.new("zomg!")] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/binary_test.rb b/activerecord/test/cases/arel/nodes/binary_test.rb new file mode 100644 index 0000000000..d160e7cd9d --- /dev/null +++ b/activerecord/test/cases/arel/nodes/binary_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class NodesTest < Arel::Spec + describe "Binary" do + describe "#hash" do + it "generates a hash based on its value" do + eq = Equality.new("foo", "bar") + eq2 = Equality.new("foo", "bar") + eq3 = Equality.new("bar", "baz") + + assert_equal eq.hash, eq2.hash + assert_not_equal eq.hash, eq3.hash + end + + it "generates a hash specific to its class" do + eq = Equality.new("foo", "bar") + neq = NotEqual.new("foo", "bar") + + assert_not_equal eq.hash, neq.hash + end + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/bind_param_test.rb b/activerecord/test/cases/arel/nodes/bind_param_test.rb new file mode 100644 index 0000000000..37a362ece4 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/bind_param_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "BindParam" do + it "is equal to other bind params with the same value" do + BindParam.new(1).must_equal(BindParam.new(1)) + BindParam.new("foo").must_equal(BindParam.new("foo")) + end + + it "is not equal to other nodes" do + BindParam.new(nil).wont_equal(Node.new) + end + + it "is not equal to bind params with different values" do + BindParam.new(1).wont_equal(BindParam.new(2)) + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb new file mode 100644 index 0000000000..89861488df --- /dev/null +++ b/activerecord/test/cases/arel/nodes/case_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class NodesTest < Arel::Spec + describe "Case" do + describe "#initialize" do + it "sets case expression from first argument" do + node = Case.new "foo" + + assert_equal "foo", node.case + end + + it "sets default case from second argument" do + node = Case.new nil, "bar" + + assert_equal "bar", node.default + end + end + + describe "#clone" do + it "clones case, conditions and default" do + foo = Nodes.build_quoted "foo" + + node = Case.new + node.case = foo + node.conditions = [When.new(foo, foo)] + node.default = foo + + dolly = node.clone + + assert_equal dolly.case, node.case + assert_not_same dolly.case, node.case + + assert_equal dolly.conditions, node.conditions + assert_not_same dolly.conditions, node.conditions + + assert_equal dolly.default, node.default + assert_not_same dolly.default, node.default + end + end + + describe "equality" do + it "is equal with equal ivars" do + foo = Nodes.build_quoted "foo" + one = Nodes.build_quoted 1 + zero = Nodes.build_quoted 0 + + case1 = Case.new foo + case1.conditions = [When.new(foo, one)] + case1.default = Else.new zero + + case2 = Case.new foo + case2.conditions = [When.new(foo, one)] + case2.default = Else.new zero + + array = [case1, case2] + + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + foo = Nodes.build_quoted "foo" + bar = Nodes.build_quoted "bar" + one = Nodes.build_quoted 1 + zero = Nodes.build_quoted 0 + + case1 = Case.new foo + case1.conditions = [When.new(foo, one)] + case1.default = Else.new zero + + case2 = Case.new foo + case2.conditions = [When.new(bar, one)] + case2.default = Else.new zero + + array = [case1, case2] + + assert_equal 2, array.uniq.size + end + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/casted_test.rb b/activerecord/test/cases/arel/nodes/casted_test.rb new file mode 100644 index 0000000000..e27f58a4e2 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/casted_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe Casted do + describe "#hash" do + it "is equal when eql? returns true" do + one = Casted.new 1, 2 + also_one = Casted.new 1, 2 + + assert_equal one.hash, also_one.hash + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/count_test.rb b/activerecord/test/cases/arel/nodes/count_test.rb new file mode 100644 index 0000000000..daabea6c4c --- /dev/null +++ b/activerecord/test/cases/arel/nodes/count_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative "../helper" + +class Arel::Nodes::CountTest < Arel::Spec + describe "as" do + it "should alias the count" do + table = Arel::Table.new :users + table[:id].count.as("foo").to_sql.must_be_like %{ + COUNT("users"."id") AS foo + } + end + end + + describe "eq" do + it "should compare the count" do + table = Arel::Table.new :users + table[:id].count.eq(2).to_sql.must_be_like %{ + COUNT("users"."id") = 2 + } + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo!")] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/delete_statement_test.rb b/activerecord/test/cases/arel/nodes/delete_statement_test.rb new file mode 100644 index 0000000000..3f078063a4 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/delete_statement_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "../helper" + +describe Arel::Nodes::DeleteStatement do + describe "#clone" do + it "clones wheres" do + statement = Arel::Nodes::DeleteStatement.new + statement.wheres = %w[a b c] + + dolly = statement.clone + dolly.wheres.must_equal statement.wheres + dolly.wheres.wont_be_same_as statement.wheres + end + end + + describe "equality" do + it "is equal with equal ivars" do + statement1 = Arel::Nodes::DeleteStatement.new + statement1.wheres = %w[a b c] + statement2 = Arel::Nodes::DeleteStatement.new + statement2.wheres = %w[a b c] + array = [statement1, statement2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + statement1 = Arel::Nodes::DeleteStatement.new + statement1.wheres = %w[a b c] + statement2 = Arel::Nodes::DeleteStatement.new + statement2.wheres = %w[1 2 3] + array = [statement1, statement2] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/descending_test.rb b/activerecord/test/cases/arel/nodes/descending_test.rb new file mode 100644 index 0000000000..5f1747e1da --- /dev/null +++ b/activerecord/test/cases/arel/nodes/descending_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestDescending < Arel::Test + def test_construct + descending = Descending.new "zomg" + assert_equal "zomg", descending.expr + end + + def test_reverse + descending = Descending.new "zomg" + ascending = descending.reverse + assert_kind_of Ascending, ascending + assert_equal descending.expr, ascending.expr + end + + def test_direction + descending = Descending.new "zomg" + assert_equal :desc, descending.direction + end + + def test_ascending? + descending = Descending.new "zomg" + assert_not descending.ascending? + end + + def test_descending? + descending = Descending.new "zomg" + assert descending.descending? + end + + def test_equality_with_same_ivars + array = [Descending.new("zomg"), Descending.new("zomg")] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [Descending.new("zomg"), Descending.new("zomg!")] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/distinct_test.rb b/activerecord/test/cases/arel/nodes/distinct_test.rb new file mode 100644 index 0000000000..de5f0ee588 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/distinct_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "Distinct" do + describe "equality" do + it "is equal to other distinct nodes" do + array = [Distinct.new, Distinct.new] + assert_equal 1, array.uniq.size + end + + it "is not equal with other nodes" do + array = [Distinct.new, Node.new] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/equality_test.rb b/activerecord/test/cases/arel/nodes/equality_test.rb new file mode 100644 index 0000000000..e173720e86 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/equality_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "equality" do + # FIXME: backwards compat + describe "backwards compat" do + describe "operator" do + it "returns :==" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + left.operator.must_equal :== + end + end + + describe "operand1" do + it "should equal left" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + left.left.must_equal left.operand1 + end + end + + describe "operand2" do + it "should equal right" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + left.right.must_equal left.operand2 + end + end + + describe "to_sql" do + it "takes an engine" do + engine = FakeRecord::Base.new + engine.connection.extend Module.new { + attr_accessor :quote_count + def quote(*args) @quote_count += 1; super; end + def quote_column_name(*args) @quote_count += 1; super; end + def quote_table_name(*args) @quote_count += 1; super; end + } + engine.connection.quote_count = 0 + + attr = Table.new(:users)[:id] + test = attr.eq(10) + test.to_sql engine + engine.connection.quote_count.must_equal 3 + end + end + end + + describe "or" do + it "makes an OR node" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + right = attr.eq(11) + node = left.or right + node.expr.left.must_equal left + node.expr.right.must_equal right + end + end + + describe "and" do + it "makes and AND node" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + right = attr.eq(11) + node = left.and right + node.left.must_equal left + node.right.must_equal right + end + end + + it "is equal with equal ivars" do + array = [Equality.new("foo", "bar"), Equality.new("foo", "bar")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Equality.new("foo", "bar"), Equality.new("foo", "baz")] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/extract_test.rb b/activerecord/test/cases/arel/nodes/extract_test.rb new file mode 100644 index 0000000000..8fc1e04d67 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/extract_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require_relative "../helper" + +class Arel::Nodes::ExtractTest < Arel::Spec + it "should extract field" do + table = Arel::Table.new :users + table[:timestamp].extract("date").to_sql.must_be_like %{ + EXTRACT(DATE FROM "users"."timestamp") + } + end + + describe "as" do + it "should alias the extract" do + table = Arel::Table.new :users + table[:timestamp].extract("date").as("foo").to_sql.must_be_like %{ + EXTRACT(DATE FROM "users"."timestamp") AS foo + } + end + + it "should not mutate the extract" do + table = Arel::Table.new :users + extract = table[:timestamp].extract("date") + before = extract.dup + extract.as("foo") + assert_equal extract, before + end + end + + describe "equality" do + it "is equal with equal ivars" do + table = Arel::Table.new :users + array = [table[:attr].extract("foo"), table[:attr].extract("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + table = Arel::Table.new :users + array = [table[:attr].extract("foo"), table[:attr].extract("bar")] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/false_test.rb b/activerecord/test/cases/arel/nodes/false_test.rb new file mode 100644 index 0000000000..4ecf8e332e --- /dev/null +++ b/activerecord/test/cases/arel/nodes/false_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "False" do + describe "equality" do + it "is equal to other false nodes" do + array = [False.new, False.new] + assert_equal 1, array.uniq.size + end + + it "is not equal with other nodes" do + array = [False.new, Node.new] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/grouping_test.rb b/activerecord/test/cases/arel/nodes/grouping_test.rb new file mode 100644 index 0000000000..03d5c142d5 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/grouping_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class GroupingTest < Arel::Spec + it "should create Equality nodes" do + grouping = Grouping.new(Nodes.build_quoted("foo")) + grouping.eq("foo").to_sql.must_be_like "('foo') = 'foo'" + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Grouping.new("foo"), Grouping.new("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Grouping.new("foo"), Grouping.new("bar")] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/infix_operation_test.rb b/activerecord/test/cases/arel/nodes/infix_operation_test.rb new file mode 100644 index 0000000000..dcf2200c12 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/infix_operation_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestInfixOperation < Arel::Test + def test_construct + operation = InfixOperation.new :+, 1, 2 + assert_equal :+, operation.operator + assert_equal 1, operation.left + assert_equal 2, operation.right + end + + def test_operation_alias + operation = InfixOperation.new :+, 1, 2 + aliaz = operation.as("zomg") + assert_kind_of As, aliaz + assert_equal operation, aliaz.left + assert_equal "zomg", aliaz.right + end + + def test_operation_ordering + operation = InfixOperation.new :+, 1, 2 + ordering = operation.desc + assert_kind_of Descending, ordering + assert_equal operation, ordering.expr + assert ordering.descending? + end + + def test_equality_with_same_ivars + array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 2)] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 3)] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/insert_statement_test.rb b/activerecord/test/cases/arel/nodes/insert_statement_test.rb new file mode 100644 index 0000000000..252a0d0d0b --- /dev/null +++ b/activerecord/test/cases/arel/nodes/insert_statement_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative "../helper" + +describe Arel::Nodes::InsertStatement do + describe "#clone" do + it "clones columns and values" do + statement = Arel::Nodes::InsertStatement.new + statement.columns = %w[a b c] + statement.values = %w[x y z] + + dolly = statement.clone + dolly.columns.must_equal statement.columns + dolly.values.must_equal statement.values + + dolly.columns.wont_be_same_as statement.columns + dolly.values.wont_be_same_as statement.values + end + end + + describe "equality" do + it "is equal with equal ivars" do + statement1 = Arel::Nodes::InsertStatement.new + statement1.columns = %w[a b c] + statement1.values = %w[x y z] + statement2 = Arel::Nodes::InsertStatement.new + statement2.columns = %w[a b c] + statement2.values = %w[x y z] + array = [statement1, statement2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + statement1 = Arel::Nodes::InsertStatement.new + statement1.columns = %w[a b c] + statement1.values = %w[x y z] + statement2 = Arel::Nodes::InsertStatement.new + statement2.columns = %w[a b c] + statement2.values = %w[1 2 3] + array = [statement1, statement2] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/named_function_test.rb b/activerecord/test/cases/arel/nodes/named_function_test.rb new file mode 100644 index 0000000000..dbd7ae43be --- /dev/null +++ b/activerecord/test/cases/arel/nodes/named_function_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestNamedFunction < Arel::Test + def test_construct + function = NamedFunction.new "omg", "zomg" + assert_equal "omg", function.name + assert_equal "zomg", function.expressions + end + + def test_function_alias + function = NamedFunction.new "omg", "zomg" + function = function.as("wth") + assert_equal "omg", function.name + assert_equal "zomg", function.expressions + assert_kind_of SqlLiteral, function.alias + assert_equal "wth", function.alias + end + + def test_construct_with_alias + function = NamedFunction.new "omg", "zomg", "wth" + assert_equal "omg", function.name + assert_equal "zomg", function.expressions + assert_kind_of SqlLiteral, function.alias + assert_equal "wth", function.alias + end + + def test_equality_with_same_ivars + array = [ + NamedFunction.new("omg", "zomg", "wth"), + NamedFunction.new("omg", "zomg", "wth") + ] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [ + NamedFunction.new("omg", "zomg", "wth"), + NamedFunction.new("zomg", "zomg", "wth") + ] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/node_test.rb b/activerecord/test/cases/arel/nodes/node_test.rb new file mode 100644 index 0000000000..f4f07ef2c5 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/node_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + class TestNode < Arel::Test + def test_includes_factory_methods + assert Node.new.respond_to?(:create_join) + end + + def test_all_nodes_are_nodes + Nodes.constants.map { |k| + Nodes.const_get(k) + }.grep(Class).each do |klass| + next if Nodes::SqlLiteral == klass + next if Nodes::BindParam == klass + next if klass.name =~ /^Arel::Nodes::(?:Test|.*Test$)/ + assert klass.ancestors.include?(Nodes::Node), klass.name + end + end + + def test_each + list = [] + node = Nodes::Node.new + node.each { |n| list << n } + assert_equal [node], list + end + + def test_generator + list = [] + node = Nodes::Node.new + node.each.each { |n| list << n } + assert_equal [node], list + end + + def test_enumerable + node = Nodes::Node.new + assert_kind_of Enumerable, node + end + end +end diff --git a/activerecord/test/cases/arel/nodes/not_test.rb b/activerecord/test/cases/arel/nodes/not_test.rb new file mode 100644 index 0000000000..481e678700 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/not_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "not" do + describe "#not" do + it "makes a NOT node" do + attr = Table.new(:users)[:id] + expr = attr.eq(10) + node = expr.not + node.must_be_kind_of Not + node.expr.must_equal expr + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Not.new("foo"), Not.new("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Not.new("foo"), Not.new("baz")] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/or_test.rb b/activerecord/test/cases/arel/nodes/or_test.rb new file mode 100644 index 0000000000..93f826740d --- /dev/null +++ b/activerecord/test/cases/arel/nodes/or_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "or" do + describe "#or" do + it "makes an OR node" do + attr = Table.new(:users)[:id] + left = attr.eq(10) + right = attr.eq(11) + node = left.or right + node.expr.left.must_equal left + node.expr.right.must_equal right + + oror = node.or(right) + oror.expr.left.must_equal node + oror.expr.right.must_equal right + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Or.new("foo", "bar"), Or.new("foo", "bar")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Or.new("foo", "bar"), Or.new("foo", "baz")] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/over_test.rb b/activerecord/test/cases/arel/nodes/over_test.rb new file mode 100644 index 0000000000..981ec2e34b --- /dev/null +++ b/activerecord/test/cases/arel/nodes/over_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require_relative "../helper" + +class Arel::Nodes::OverTest < Arel::Spec + describe "as" do + it "should alias the expression" do + table = Arel::Table.new :users + table[:id].count.over.as("foo").to_sql.must_be_like %{ + COUNT("users"."id") OVER () AS foo + } + end + end + + describe "with literal" do + it "should reference the window definition by name" do + table = Arel::Table.new :users + table[:id].count.over("foo").to_sql.must_be_like %{ + COUNT("users"."id") OVER "foo" + } + end + end + + describe "with SQL literal" do + it "should reference the window definition by name" do + table = Arel::Table.new :users + table[:id].count.over(Arel.sql("foo")).to_sql.must_be_like %{ + COUNT("users"."id") OVER foo + } + end + end + + describe "with no expression" do + it "should use empty definition" do + table = Arel::Table.new :users + table[:id].count.over.to_sql.must_be_like %{ + COUNT("users"."id") OVER () + } + end + end + + describe "with expression" do + it "should use definition in sub-expression" do + table = Arel::Table.new :users + window = Arel::Nodes::Window.new.order(table["foo"]) + table[:id].count.over(window).to_sql.must_be_like %{ + COUNT("users"."id") OVER (ORDER BY \"users\".\"foo\") + } + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [ + Arel::Nodes::Over.new("foo", "bar"), + Arel::Nodes::Over.new("foo", "bar") + ] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [ + Arel::Nodes::Over.new("foo", "bar"), + Arel::Nodes::Over.new("foo", "baz") + ] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/select_core_test.rb b/activerecord/test/cases/arel/nodes/select_core_test.rb new file mode 100644 index 0000000000..0b698205ff --- /dev/null +++ b/activerecord/test/cases/arel/nodes/select_core_test.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestSelectCore < Arel::Test + def test_clone + core = Arel::Nodes::SelectCore.new + core.froms = %w[a b c] + core.projections = %w[d e f] + core.wheres = %w[g h i] + + dolly = core.clone + + assert_equal core.froms, dolly.froms + assert_equal core.projections, dolly.projections + assert_equal core.wheres, dolly.wheres + + assert_not_same core.froms, dolly.froms + assert_not_same core.projections, dolly.projections + assert_not_same core.wheres, dolly.wheres + end + + def test_set_quantifier + core = Arel::Nodes::SelectCore.new + core.set_quantifier = Arel::Nodes::Distinct.new + viz = Arel::Visitors::ToSql.new Table.engine.connection_pool + assert_match "DISTINCT", viz.accept(core, Collectors::SQLString.new).value + end + + def test_equality_with_same_ivars + core1 = SelectCore.new + core1.froms = %w[a b c] + core1.projections = %w[d e f] + core1.wheres = %w[g h i] + core1.groups = %w[j k l] + core1.windows = %w[m n o] + core1.havings = %w[p q r] + core2 = SelectCore.new + core2.froms = %w[a b c] + core2.projections = %w[d e f] + core2.wheres = %w[g h i] + core2.groups = %w[j k l] + core2.windows = %w[m n o] + core2.havings = %w[p q r] + array = [core1, core2] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + core1 = SelectCore.new + core1.froms = %w[a b c] + core1.projections = %w[d e f] + core1.wheres = %w[g h i] + core1.groups = %w[j k l] + core1.windows = %w[m n o] + core1.havings = %w[p q r] + core2 = SelectCore.new + core2.froms = %w[a b c] + core2.projections = %w[d e f] + core2.wheres = %w[g h i] + core2.groups = %w[j k l] + core2.windows = %w[m n o] + core2.havings = %w[l o l] + array = [core1, core2] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/select_statement_test.rb b/activerecord/test/cases/arel/nodes/select_statement_test.rb new file mode 100644 index 0000000000..a91605de3e --- /dev/null +++ b/activerecord/test/cases/arel/nodes/select_statement_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative "../helper" + +describe Arel::Nodes::SelectStatement do + describe "#clone" do + it "clones cores" do + statement = Arel::Nodes::SelectStatement.new %w[a b c] + + dolly = statement.clone + dolly.cores.must_equal statement.cores + dolly.cores.wont_be_same_as statement.cores + end + end + + describe "equality" do + it "is equal with equal ivars" do + statement1 = Arel::Nodes::SelectStatement.new %w[a b c] + statement1.offset = 1 + statement1.limit = 2 + statement1.lock = false + statement1.orders = %w[x y z] + statement1.with = "zomg" + statement2 = Arel::Nodes::SelectStatement.new %w[a b c] + statement2.offset = 1 + statement2.limit = 2 + statement2.lock = false + statement2.orders = %w[x y z] + statement2.with = "zomg" + array = [statement1, statement2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + statement1 = Arel::Nodes::SelectStatement.new %w[a b c] + statement1.offset = 1 + statement1.limit = 2 + statement1.lock = false + statement1.orders = %w[x y z] + statement1.with = "zomg" + statement2 = Arel::Nodes::SelectStatement.new %w[a b c] + statement2.offset = 1 + statement2.limit = 2 + statement2.lock = false + statement2.orders = %w[x y z] + statement2.with = "wth" + array = [statement1, statement2] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/sql_literal_test.rb b/activerecord/test/cases/arel/nodes/sql_literal_test.rb new file mode 100644 index 0000000000..3b95fed1f4 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/sql_literal_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "yaml" + +module Arel + module Nodes + class SqlLiteralTest < Arel::Spec + before do + @visitor = Visitors::ToSql.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + describe "sql" do + it "makes a sql literal node" do + sql = Arel.sql "foo" + sql.must_be_kind_of Arel::Nodes::SqlLiteral + end + end + + describe "count" do + it "makes a count node" do + node = SqlLiteral.new("*").count + compile(node).must_be_like %{ COUNT(*) } + end + + it "makes a distinct node" do + node = SqlLiteral.new("*").count true + compile(node).must_be_like %{ COUNT(DISTINCT *) } + end + end + + describe "equality" do + it "makes an equality node" do + node = SqlLiteral.new("foo").eq(1) + compile(node).must_be_like %{ foo = 1 } + end + + it "is equal with equal contents" do + array = [SqlLiteral.new("foo"), SqlLiteral.new("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different contents" do + array = [SqlLiteral.new("foo"), SqlLiteral.new("bar")] + assert_equal 2, array.uniq.size + end + end + + describe 'grouped "or" equality' do + it "makes a grouping node with an or node" do + node = SqlLiteral.new("foo").eq_any([1, 2]) + compile(node).must_be_like %{ (foo = 1 OR foo = 2) } + end + end + + describe 'grouped "and" equality' do + it "makes a grouping node with an and node" do + node = SqlLiteral.new("foo").eq_all([1, 2]) + compile(node).must_be_like %{ (foo = 1 AND foo = 2) } + end + end + + describe "serialization" do + it "serializes into YAML" do + yaml_literal = SqlLiteral.new("foo").to_yaml + assert_equal("foo", YAML.load(yaml_literal)) + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/sum_test.rb b/activerecord/test/cases/arel/nodes/sum_test.rb new file mode 100644 index 0000000000..5015964951 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/sum_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative "../helper" + +class Arel::Nodes::SumTest < Arel::Spec + describe "as" do + it "should alias the sum" do + table = Arel::Table.new :users + table[:id].sum.as("foo").to_sql.must_be_like %{ + SUM("users"."id") AS foo + } + end + end + + describe "equality" do + it "is equal with equal ivars" do + array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo")] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo!")] + assert_equal 2, array.uniq.size + end + end + + describe "order" do + it "should order the sum" do + table = Arel::Table.new :users + table[:id].sum.desc.to_sql.must_be_like %{ + SUM("users"."id") DESC + } + end + end +end diff --git a/activerecord/test/cases/arel/nodes/table_alias_test.rb b/activerecord/test/cases/arel/nodes/table_alias_test.rb new file mode 100644 index 0000000000..c661b6771e --- /dev/null +++ b/activerecord/test/cases/arel/nodes/table_alias_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "table alias" do + describe "equality" do + it "is equal with equal ivars" do + relation1 = Table.new(:users) + node1 = TableAlias.new relation1, :foo + relation2 = Table.new(:users) + node2 = TableAlias.new relation2, :foo + array = [node1, node2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + relation1 = Table.new(:users) + node1 = TableAlias.new relation1, :foo + relation2 = Table.new(:users) + node2 = TableAlias.new relation2, :bar + array = [node1, node2] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/true_test.rb b/activerecord/test/cases/arel/nodes/true_test.rb new file mode 100644 index 0000000000..1e85fe7d48 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/true_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "True" do + describe "equality" do + it "is equal to other true nodes" do + array = [True.new, True.new] + assert_equal 1, array.uniq.size + end + + it "is not equal with other nodes" do + array = [True.new, Node.new] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/unary_operation_test.rb b/activerecord/test/cases/arel/nodes/unary_operation_test.rb new file mode 100644 index 0000000000..f0dd0c625c --- /dev/null +++ b/activerecord/test/cases/arel/nodes/unary_operation_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + class TestUnaryOperation < Arel::Test + def test_construct + operation = UnaryOperation.new :-, 1 + assert_equal :-, operation.operator + assert_equal 1, operation.expr + end + + def test_operation_alias + operation = UnaryOperation.new :-, 1 + aliaz = operation.as("zomg") + assert_kind_of As, aliaz + assert_equal operation, aliaz.left + assert_equal "zomg", aliaz.right + end + + def test_operation_ordering + operation = UnaryOperation.new :-, 1 + ordering = operation.desc + assert_kind_of Descending, ordering + assert_equal operation, ordering.expr + assert ordering.descending? + end + + def test_equality_with_same_ivars + array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 1)] + assert_equal 1, array.uniq.size + end + + def test_inequality_with_different_ivars + array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 2)] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/update_statement_test.rb b/activerecord/test/cases/arel/nodes/update_statement_test.rb new file mode 100644 index 0000000000..a83ce32f68 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/update_statement_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative "../helper" + +describe Arel::Nodes::UpdateStatement do + describe "#clone" do + it "clones wheres and values" do + statement = Arel::Nodes::UpdateStatement.new + statement.wheres = %w[a b c] + statement.values = %w[x y z] + + dolly = statement.clone + dolly.wheres.must_equal statement.wheres + dolly.wheres.wont_be_same_as statement.wheres + + dolly.values.must_equal statement.values + dolly.values.wont_be_same_as statement.values + end + end + + describe "equality" do + it "is equal with equal ivars" do + statement1 = Arel::Nodes::UpdateStatement.new + statement1.relation = "zomg" + statement1.wheres = 2 + statement1.values = false + statement1.orders = %w[x y z] + statement1.limit = 42 + statement1.key = "zomg" + statement2 = Arel::Nodes::UpdateStatement.new + statement2.relation = "zomg" + statement2.wheres = 2 + statement2.values = false + statement2.orders = %w[x y z] + statement2.limit = 42 + statement2.key = "zomg" + array = [statement1, statement2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + statement1 = Arel::Nodes::UpdateStatement.new + statement1.relation = "zomg" + statement1.wheres = 2 + statement1.values = false + statement1.orders = %w[x y z] + statement1.limit = 42 + statement1.key = "zomg" + statement2 = Arel::Nodes::UpdateStatement.new + statement2.relation = "zomg" + statement2.wheres = 2 + statement2.values = false + statement2.orders = %w[x y z] + statement2.limit = 42 + statement2.key = "wth" + array = [statement1, statement2] + assert_equal 2, array.uniq.size + end + end +end diff --git a/activerecord/test/cases/arel/nodes/window_test.rb b/activerecord/test/cases/arel/nodes/window_test.rb new file mode 100644 index 0000000000..729b0556a4 --- /dev/null +++ b/activerecord/test/cases/arel/nodes/window_test.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Nodes + describe "Window" do + describe "equality" do + it "is equal with equal ivars" do + window1 = Window.new + window1.orders = [1, 2] + window1.partitions = [1] + window1.frame 3 + window2 = Window.new + window2.orders = [1, 2] + window2.partitions = [1] + window2.frame 3 + array = [window1, window2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + window1 = Window.new + window1.orders = [1, 2] + window1.partitions = [1] + window1.frame 3 + window2 = Window.new + window2.orders = [1, 2] + window1.partitions = [1] + window2.frame 4 + array = [window1, window2] + assert_equal 2, array.uniq.size + end + end + end + + describe "NamedWindow" do + describe "equality" do + it "is equal with equal ivars" do + window1 = NamedWindow.new "foo" + window1.orders = [1, 2] + window1.partitions = [1] + window1.frame 3 + window2 = NamedWindow.new "foo" + window2.orders = [1, 2] + window2.partitions = [1] + window2.frame 3 + array = [window1, window2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + window1 = NamedWindow.new "foo" + window1.orders = [1, 2] + window1.partitions = [1] + window1.frame 3 + window2 = NamedWindow.new "bar" + window2.orders = [1, 2] + window2.partitions = [1] + window2.frame 3 + array = [window1, window2] + assert_equal 2, array.uniq.size + end + end + end + + describe "CurrentRow" do + describe "equality" do + it "is equal to other current row nodes" do + array = [CurrentRow.new, CurrentRow.new] + assert_equal 1, array.uniq.size + end + + it "is not equal with other nodes" do + array = [CurrentRow.new, Node.new] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes_test.rb b/activerecord/test/cases/arel/nodes_test.rb new file mode 100644 index 0000000000..9021de0d20 --- /dev/null +++ b/activerecord/test/cases/arel/nodes_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + module Nodes + class TestNodes < Arel::Test + def test_every_arel_nodes_have_hash_eql_eqeq_from_same_class + # #descendants code from activesupport + node_descendants = [] + ObjectSpace.each_object(Arel::Nodes::Node.singleton_class) do |k| + next if k.respond_to?(:singleton_class?) && k.singleton_class? + node_descendants.unshift k unless k == self + end + node_descendants.delete(Arel::Nodes::Node) + node_descendants.delete(Arel::Nodes::NodeExpression) + + bad_node_descendants = node_descendants.reject do |subnode| + eqeq_owner = subnode.instance_method(:==).owner + eql_owner = subnode.instance_method(:eql?).owner + hash_owner = subnode.instance_method(:hash).owner + + eqeq_owner < Arel::Nodes::Node && + eqeq_owner == eql_owner && + eqeq_owner == hash_owner + end + + problem_msg = "Some subclasses of Arel::Nodes::Node do not have a" \ + " #== or #eql? or #hash defined from the same class as the others" + assert_empty bad_node_descendants, problem_msg + end + end + end +end diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb new file mode 100644 index 0000000000..c17487ae88 --- /dev/null +++ b/activerecord/test/cases/arel/select_manager_test.rb @@ -0,0 +1,1225 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class SelectManagerTest < Arel::Spec + def test_join_sources + manager = Arel::SelectManager.new + manager.join_sources << Arel::Nodes::StringJoin.new(Nodes.build_quoted("foo")) + assert_equal "SELECT FROM 'foo'", manager.to_sql + end + + describe "backwards compatibility" do + describe "project" do + it "accepts symbols as sql literals" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.project :id + manager.from table + manager.to_sql.must_be_like %{ + SELECT id FROM "users" + } + end + end + + describe "order" do + it "accepts symbols" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new "*" + manager.from table + manager.order :foo + manager.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo } + end + end + + describe "group" do + it "takes a symbol" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.group :foo + manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo } + end + end + + describe "as" do + it "makes an AS node by grouping the AST" do + manager = Arel::SelectManager.new + as = manager.as(Arel.sql("foo")) + assert_kind_of Arel::Nodes::Grouping, as.left + assert_equal manager.ast, as.left.expr + assert_equal "foo", as.right + end + + it "converts right to SqlLiteral if a string" do + manager = Arel::SelectManager.new + as = manager.as("foo") + assert_kind_of Arel::Nodes::SqlLiteral, as.right + end + + it "can make a subselect" do + manager = Arel::SelectManager.new + manager.project Arel.star + manager.from Arel.sql("zomg") + as = manager.as(Arel.sql("foo")) + + manager = Arel::SelectManager.new + manager.project Arel.sql("name") + manager.from as + manager.to_sql.must_be_like "SELECT name FROM (SELECT * FROM zomg) foo" + end + end + + describe "from" do + it "ignores strings when table of same name exists" do + table = Table.new :users + manager = Arel::SelectManager.new + + manager.from table + manager.from "users" + manager.project table["id"] + manager.to_sql.must_be_like 'SELECT "users"."id" FROM users' + end + + it "should support any ast" do + table = Table.new :users + manager1 = Arel::SelectManager.new + + manager2 = Arel::SelectManager.new + manager2.project(Arel.sql("*")) + manager2.from table + + manager1.project Arel.sql("lol") + as = manager2.as Arel.sql("omg") + manager1.from(as) + + manager1.to_sql.must_be_like %{ + SELECT lol FROM (SELECT * FROM "users") omg + } + end + end + + describe "having" do + it "converts strings to SQLLiterals" do + table = Table.new :users + mgr = table.from + mgr.having Arel.sql("foo") + mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo } + end + + it "can have multiple items specified separately" do + table = Table.new :users + mgr = table.from + mgr.having Arel.sql("foo") + mgr.having Arel.sql("bar") + mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar } + end + + it "can receive any node" do + table = Table.new :users + mgr = table.from + mgr.having Arel::Nodes::And.new([Arel.sql("foo"), Arel.sql("bar")]) + mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar } + end + end + + describe "on" do + it "converts to sqlliterals" do + table = Table.new :users + right = table.alias + mgr = table.from + mgr.join(right).on("omg") + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg } + end + + it "converts to sqlliterals with multiple items" do + table = Table.new :users + right = table.alias + mgr = table.from + mgr.join(right).on("omg", "123") + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 } + end + end + end + + describe "clone" do + it "creates new cores" do + table = Table.new :users, as: "foo" + mgr = table.from + m2 = mgr.clone + m2.project "foo" + mgr.to_sql.wont_equal m2.to_sql + end + + it "makes updates to the correct copy" do + table = Table.new :users, as: "foo" + mgr = table.from + m2 = mgr.clone + m3 = m2.clone + m2.project "foo" + mgr.to_sql.wont_equal m2.to_sql + m3.to_sql.must_equal mgr.to_sql + end + end + + describe "initialize" do + it "uses alias in sql" do + table = Table.new :users, as: "foo" + mgr = table.from + mgr.skip 10 + mgr.to_sql.must_be_like %{ SELECT FROM "users" "foo" OFFSET 10 } + end + end + + describe "skip" do + it "should add an offset" do + table = Table.new :users + mgr = table.from + mgr.skip 10 + mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 } + end + + it "should chain" do + table = Table.new :users + mgr = table.from + mgr.skip(10).to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 } + end + end + + describe "offset" do + it "should add an offset" do + table = Table.new :users + mgr = table.from + mgr.offset = 10 + mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 } + end + + it "should remove an offset" do + table = Table.new :users + mgr = table.from + mgr.offset = 10 + mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 } + + mgr.offset = nil + mgr.to_sql.must_be_like %{ SELECT FROM "users" } + end + + it "should return the offset" do + table = Table.new :users + mgr = table.from + mgr.offset = 10 + assert_equal 10, mgr.offset + end + end + + describe "exists" do + it "should create an exists clause" do + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.project Nodes::SqlLiteral.new "*" + m2 = Arel::SelectManager.new + m2.project manager.exists + m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) } + end + + it "can be aliased" do + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.project Nodes::SqlLiteral.new "*" + m2 = Arel::SelectManager.new + m2.project manager.exists.as("foo") + m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) AS foo } + end + end + + describe "union" do + before do + table = Table.new :users + @m1 = Arel::SelectManager.new table + @m1.project Arel.star + @m1.where(table[:age].lt(18)) + + @m2 = Arel::SelectManager.new table + @m2.project Arel.star + @m2.where(table[:age].gt(99)) + end + + it "should union two managers" do + # FIXME should this union "managers" or "statements" ? + # FIXME this probably shouldn't return a node + node = @m1.union @m2 + + # maybe FIXME: decide when wrapper parens are needed + node.to_sql.must_be_like %{ + ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION SELECT * FROM "users" WHERE "users"."age" > 99 ) + } + end + + it "should union all" do + node = @m1.union :all, @m2 + + node.to_sql.must_be_like %{ + ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 ) + } + end + end + + describe "intersect" do + before do + table = Table.new :users + @m1 = Arel::SelectManager.new table + @m1.project Arel.star + @m1.where(table[:age].gt(18)) + + @m2 = Arel::SelectManager.new table + @m2.project Arel.star + @m2.where(table[:age].lt(99)) + end + + it "should intersect two managers" do + # FIXME should this intersect "managers" or "statements" ? + # FIXME this probably shouldn't return a node + node = @m1.intersect @m2 + + # maybe FIXME: decide when wrapper parens are needed + node.to_sql.must_be_like %{ + ( SELECT * FROM "users" WHERE "users"."age" > 18 INTERSECT SELECT * FROM "users" WHERE "users"."age" < 99 ) + } + end + end + + describe "except" do + before do + table = Table.new :users + @m1 = Arel::SelectManager.new table + @m1.project Arel.star + @m1.where(table[:age].between(18..60)) + + @m2 = Arel::SelectManager.new table + @m2.project Arel.star + @m2.where(table[:age].between(40..99)) + end + + it "should except two managers" do + # FIXME should this except "managers" or "statements" ? + # FIXME this probably shouldn't return a node + node = @m1.except @m2 + + # maybe FIXME: decide when wrapper parens are needed + node.to_sql.must_be_like %{ + ( SELECT * FROM "users" WHERE "users"."age" BETWEEN 18 AND 60 EXCEPT SELECT * FROM "users" WHERE "users"."age" BETWEEN 40 AND 99 ) + } + end + end + + describe "with" do + it "should support basic WITH" do + users = Table.new(:users) + users_top = Table.new(:users_top) + comments = Table.new(:comments) + + top = users.project(users[:id]).where(users[:karma].gt(100)) + users_as = Arel::Nodes::As.new(users_top, top) + select_manager = comments.project(Arel.star).with(users_as) + .where(comments[:author_id].in(users_top.project(users_top[:id]))) + + select_manager.to_sql.must_be_like %{ + WITH "users_top" AS (SELECT "users"."id" FROM "users" WHERE "users"."karma" > 100) SELECT * FROM "comments" WHERE "comments"."author_id" IN (SELECT "users_top"."id" FROM "users_top") + } + end + + it "should support WITH RECURSIVE" do + comments = Table.new(:comments) + comments_id = comments[:id] + comments_parent_id = comments[:parent_id] + + replies = Table.new(:replies) + replies_id = replies[:id] + + recursive_term = Arel::SelectManager.new + recursive_term.from(comments).project(comments_id, comments_parent_id).where(comments_id.eq 42) + + non_recursive_term = Arel::SelectManager.new + non_recursive_term.from(comments).project(comments_id, comments_parent_id).join(replies).on(comments_parent_id.eq replies_id) + + union = recursive_term.union(non_recursive_term) + + as_statement = Arel::Nodes::As.new replies, union + + manager = Arel::SelectManager.new + manager.with(:recursive, as_statement).from(replies).project(Arel.star) + + sql = manager.to_sql + sql.must_be_like %{ + WITH RECURSIVE "replies" AS ( + SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42 + UNION + SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id" + ) + SELECT * FROM "replies" + } + end + end + + describe "ast" do + it "should return the ast" do + table = Table.new :users + mgr = table.from + assert mgr.ast + end + + it "should allow orders to work when the ast is grepped" do + table = Table.new :users + mgr = table.from + mgr.project Arel.sql "*" + mgr.from table + mgr.orders << Arel::Nodes::Ascending.new(Arel.sql("foo")) + mgr.ast.grep(Arel::Nodes::OuterJoin) + mgr.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo ASC } + end + end + + describe "taken" do + it "should return limit" do + manager = Arel::SelectManager.new + manager.take 10 + manager.taken.must_equal 10 + end + end + + describe "lock" do + # This should fail on other databases + it "adds a lock node" do + table = Table.new :users + mgr = table.from + mgr.lock.to_sql.must_be_like %{ SELECT FROM "users" FOR UPDATE } + end + end + + describe "orders" do + it "returns order clauses" do + table = Table.new :users + manager = Arel::SelectManager.new + order = table[:id] + manager.order table[:id] + manager.orders.must_equal [order] + end + end + + describe "order" do + it "generates order clauses" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new "*" + manager.from table + manager.order table[:id] + manager.to_sql.must_be_like %{ + SELECT * FROM "users" ORDER BY "users"."id" + } + end + + # FIXME: I would like to deprecate this + it "takes *args" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new "*" + manager.from table + manager.order table[:id], table[:name] + manager.to_sql.must_be_like %{ + SELECT * FROM "users" ORDER BY "users"."id", "users"."name" + } + end + + it "chains" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.order(table[:id]).must_equal manager + end + + it "has order attributes" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new "*" + manager.from table + manager.order table[:id].desc + manager.to_sql.must_be_like %{ + SELECT * FROM "users" ORDER BY "users"."id" DESC + } + end + end + + describe "on" do + it "takes two params" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right).on(predicate, predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + INNER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" AND + "users"."id" = "users_2"."id" + } + end + + it "takes three params" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right).on( + predicate, + predicate, + left[:name].eq(right[:name]) + ) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + INNER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" AND + "users"."id" = "users_2"."id" AND + "users"."name" = "users_2"."name" + } + end + end + + it "should hand back froms" do + relation = Arel::SelectManager.new + assert_equal [], relation.froms + end + + it "should create and nodes" do + relation = Arel::SelectManager.new + children = ["foo", "bar", "baz"] + clause = relation.create_and children + assert_kind_of Arel::Nodes::And, clause + assert_equal children, clause.children + end + + it "should create insert managers" do + relation = Arel::SelectManager.new + insert = relation.create_insert + assert_kind_of Arel::InsertManager, insert + end + + it "should create join nodes" do + relation = Arel::SelectManager.new + join = relation.create_join "foo", "bar" + assert_kind_of Arel::Nodes::InnerJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a full outer join klass" do + relation = Arel::SelectManager.new + join = relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin + assert_kind_of Arel::Nodes::FullOuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a outer join klass" do + relation = Arel::SelectManager.new + join = relation.create_join "foo", "bar", Arel::Nodes::OuterJoin + assert_kind_of Arel::Nodes::OuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a right outer join klass" do + relation = Arel::SelectManager.new + join = relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin + assert_kind_of Arel::Nodes::RightOuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + describe "join" do + it "responds to join" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right).on(predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + INNER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "takes a class" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right, Nodes::OuterJoin).on(predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + LEFT OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "takes the full outer join class" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right, Nodes::FullOuterJoin).on(predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + FULL OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "takes the right outer join class" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.join(right, Nodes::RightOuterJoin).on(predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + RIGHT OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "noops on nil" do + manager = Arel::SelectManager.new + manager.join(nil).must_equal manager + end + + it "raises EmptyJoinError on empty" do + left = Table.new :users + manager = Arel::SelectManager.new + + manager.from left + assert_raises(EmptyJoinError) do + manager.join("") + end + end + end + + describe "outer join" do + it "responds to join" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + manager = Arel::SelectManager.new + + manager.from left + manager.outer_join(right).on(predicate) + manager.to_sql.must_be_like %{ + SELECT FROM "users" + LEFT OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "noops on nil" do + manager = Arel::SelectManager.new + manager.outer_join(nil).must_equal manager + end + end + + describe "joins" do + it "returns inner join sql" do + table = Table.new :users + aliaz = table.alias + manager = Arel::SelectManager.new + manager.from Nodes::InnerJoin.new(aliaz, table[:id].eq(aliaz[:id])) + assert_match 'INNER JOIN "users" "users_2" "users"."id" = "users_2"."id"', + manager.to_sql + end + + it "returns outer join sql" do + table = Table.new :users + aliaz = table.alias + manager = Arel::SelectManager.new + manager.from Nodes::OuterJoin.new(aliaz, table[:id].eq(aliaz[:id])) + assert_match 'LEFT OUTER JOIN "users" "users_2" "users"."id" = "users_2"."id"', + manager.to_sql + end + + it "can have a non-table alias as relation name" do + users = Table.new :users + comments = Table.new :comments + + counts = comments.from. + group(comments[:user_id]). + project( + comments[:user_id].as("user_id"), + comments[:user_id].count.as("count") + ).as("counts") + + joins = users.join(counts).on(counts[:user_id].eq(10)) + joins.to_sql.must_be_like %{ + SELECT FROM "users" INNER JOIN (SELECT "comments"."user_id" AS user_id, COUNT("comments"."user_id") AS count FROM "comments" GROUP BY "comments"."user_id") counts ON counts."user_id" = 10 + } + end + + it "joins itself" do + left = Table.new :users + right = left.alias + predicate = left[:id].eq(right[:id]) + + mgr = left.join(right) + mgr.project Nodes::SqlLiteral.new("*") + mgr.on(predicate).must_equal mgr + + mgr.to_sql.must_be_like %{ + SELECT * FROM "users" + INNER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + + it "returns string join sql" do + manager = Arel::SelectManager.new + manager.from Nodes::StringJoin.new(Nodes.build_quoted("hello")) + assert_match "'hello'", manager.to_sql + end + end + + describe "group" do + it "takes an attribute" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.group table[:id] + manager.to_sql.must_be_like %{ + SELECT FROM "users" GROUP BY "users"."id" + } + end + + it "chains" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.group(table[:id]).must_equal manager + end + + it "takes multiple args" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.group table[:id], table[:name] + manager.to_sql.must_be_like %{ + SELECT FROM "users" GROUP BY "users"."id", "users"."name" + } + end + + # FIXME: backwards compat + it "makes strings literals" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.group "foo" + manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo } + end + end + + describe "window definition" do + it "can be empty" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window") + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS () + } + end + + it "takes an order" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").order(table["foo"].asc) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC) + } + end + + it "takes an order with multiple columns" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").order(table["foo"].asc, table["bar"].desc) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC, "users"."bar" DESC) + } + end + + it "takes a partition" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").partition(table["bar"]) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar") + } + end + + it "takes a partition and an order" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").partition(table["foo"]).order(table["foo"].asc) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."foo" + ORDER BY "users"."foo" ASC) + } + end + + it "takes a partition with multiple columns" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").partition(table["bar"], table["baz"]) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar", "users"."baz") + } + end + + it "takes a rows frame, unbounded preceding" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").rows(Arel::Nodes::Preceding.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED PRECEDING) + } + end + + it "takes a rows frame, bounded preceding" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").rows(Arel::Nodes::Preceding.new(5)) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 PRECEDING) + } + end + + it "takes a rows frame, unbounded following" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").rows(Arel::Nodes::Following.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED FOLLOWING) + } + end + + it "takes a rows frame, bounded following" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").rows(Arel::Nodes::Following.new(5)) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 FOLLOWING) + } + end + + it "takes a rows frame, current row" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").rows(Arel::Nodes::CurrentRow.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS CURRENT ROW) + } + end + + it "takes a rows frame, between two delimiters" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + window = manager.window("a_window") + window.frame( + Arel::Nodes::Between.new( + window.rows, + Nodes::And.new([ + Arel::Nodes::Preceding.new, + Arel::Nodes::CurrentRow.new + ]))) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + } + end + + it "takes a range frame, unbounded preceding" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").range(Arel::Nodes::Preceding.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED PRECEDING) + } + end + + it "takes a range frame, bounded preceding" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").range(Arel::Nodes::Preceding.new(5)) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 PRECEDING) + } + end + + it "takes a range frame, unbounded following" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").range(Arel::Nodes::Following.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED FOLLOWING) + } + end + + it "takes a range frame, bounded following" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").range(Arel::Nodes::Following.new(5)) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 FOLLOWING) + } + end + + it "takes a range frame, current row" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.window("a_window").range(Arel::Nodes::CurrentRow.new) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE CURRENT ROW) + } + end + + it "takes a range frame, between two delimiters" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + window = manager.window("a_window") + window.frame( + Arel::Nodes::Between.new( + window.range, + Nodes::And.new([ + Arel::Nodes::Preceding.new, + Arel::Nodes::CurrentRow.new + ]))) + manager.to_sql.must_be_like %{ + SELECT FROM "users" WINDOW "a_window" AS (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + } + end + end + + describe "delete" do + it "copies from" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + stmt = manager.compile_delete + + stmt.to_sql.must_be_like %{ DELETE FROM "users" } + end + + it "copies where" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.where table[:id].eq 10 + stmt = manager.compile_delete + + stmt.to_sql.must_be_like %{ + DELETE FROM "users" WHERE "users"."id" = 10 + } + end + end + + describe "where_sql" do + it "gives me back the where sql" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.where table[:id].eq 10 + manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 } + end + + it "joins wheres with AND" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.where table[:id].eq 10 + manager.where table[:id].eq 11 + manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."id" = 11} + end + + it "handles database specific statements" do + old_visitor = Table.engine.connection.visitor + Table.engine.connection.visitor = Visitors::PostgreSQL.new Table.engine.connection + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.where table[:id].eq 10 + manager.where table[:name].matches "foo%" + manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."name" ILIKE 'foo%' } + Table.engine.connection.visitor = old_visitor + end + + it "returns nil when there are no wheres" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.where_sql.must_be_nil + end + end + + describe "update" do + it "creates an update statement" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id")) + + stmt.to_sql.must_be_like %{ + UPDATE "users" SET "id" = 1 + } + end + + it "takes a string" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id")) + + stmt.to_sql.must_be_like %{ UPDATE "users" SET foo = bar } + end + + it "copies limits" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.take 1 + stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id")) + stmt.key = table["id"] + + stmt.to_sql.must_be_like %{ + UPDATE "users" SET foo = bar + WHERE "users"."id" IN (SELECT "users"."id" FROM "users" LIMIT 1) + } + end + + it "copies order" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from table + manager.order :foo + stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id")) + stmt.key = table["id"] + + stmt.to_sql.must_be_like %{ + UPDATE "users" SET foo = bar + WHERE "users"."id" IN (SELECT "users"."id" FROM "users" ORDER BY foo) + } + end + + it "copies where clauses" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.where table[:id].eq 10 + manager.from table + stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id")) + + stmt.to_sql.must_be_like %{ + UPDATE "users" SET "id" = 1 WHERE "users"."id" = 10 + } + end + + it "copies where clauses when nesting is triggered" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.where table[:foo].eq 10 + manager.take 42 + manager.from table + stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id")) + + stmt.to_sql.must_be_like %{ + UPDATE "users" SET "id" = 1 WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."foo" = 10 LIMIT 42) + } + end + end + + describe "project" do + it "takes sql literals" do + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new "*" + manager.to_sql.must_be_like %{ SELECT * } + end + + it "takes multiple args" do + manager = Arel::SelectManager.new + manager.project Nodes::SqlLiteral.new("foo"), + Nodes::SqlLiteral.new("bar") + manager.to_sql.must_be_like %{ SELECT foo, bar } + end + + it "takes strings" do + manager = Arel::SelectManager.new + manager.project "*" + manager.to_sql.must_be_like %{ SELECT * } + end + end + + describe "projections" do + it "reads projections" do + manager = Arel::SelectManager.new + manager.project Arel.sql("foo"), Arel.sql("bar") + manager.projections.must_equal [Arel.sql("foo"), Arel.sql("bar")] + end + end + + describe "projections=" do + it "overwrites projections" do + manager = Arel::SelectManager.new + manager.project Arel.sql("foo") + manager.projections = [Arel.sql("bar")] + manager.to_sql.must_be_like %{ SELECT bar } + end + end + + describe "take" do + it "knows take" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from(table).project(table["id"]) + manager.where(table["id"].eq(1)) + manager.take 1 + + manager.to_sql.must_be_like %{ + SELECT "users"."id" + FROM "users" + WHERE "users"."id" = 1 + LIMIT 1 + } + end + + it "chains" do + manager = Arel::SelectManager.new + manager.take(1).must_equal manager + end + + it "removes LIMIT when nil is passed" do + manager = Arel::SelectManager.new + manager.limit = 10 + assert_match("LIMIT", manager.to_sql) + + manager.limit = nil + assert_no_match("LIMIT", manager.to_sql) + end + end + + describe "where" do + it "knows where" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from(table).project(table["id"]) + manager.where(table["id"].eq(1)) + manager.to_sql.must_be_like %{ + SELECT "users"."id" + FROM "users" + WHERE "users"."id" = 1 + } + end + + it "chains" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from(table) + manager.project(table["id"]).where(table["id"].eq 1).must_equal manager + end + end + + describe "from" do + it "makes sql" do + table = Table.new :users + manager = Arel::SelectManager.new + + manager.from table + manager.project table["id"] + manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"' + end + + it "chains" do + table = Table.new :users + manager = Arel::SelectManager.new + manager.from(table).project(table["id"]).must_equal manager + manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"' + end + end + + describe "source" do + it "returns the join source of the select core" do + manager = Arel::SelectManager.new + manager.source.must_equal manager.ast.cores.last.source + end + end + + describe "distinct" do + it "sets the quantifier" do + manager = Arel::SelectManager.new + + manager.distinct + manager.ast.cores.last.set_quantifier.class.must_equal Arel::Nodes::Distinct + + manager.distinct(false) + manager.ast.cores.last.set_quantifier.must_be_nil + end + + it "chains" do + manager = Arel::SelectManager.new + manager.distinct.must_equal manager + manager.distinct(false).must_equal manager + end + end + + describe "distinct_on" do + it "sets the quantifier" do + manager = Arel::SelectManager.new + table = Table.new :users + + manager.distinct_on(table["id"]) + manager.ast.cores.last.set_quantifier.must_equal Arel::Nodes::DistinctOn.new(table["id"]) + + manager.distinct_on(false) + manager.ast.cores.last.set_quantifier.must_be_nil + end + + it "chains" do + manager = Arel::SelectManager.new + table = Table.new :users + + manager.distinct_on(table["id"]).must_equal manager + manager.distinct_on(false).must_equal manager + end + end + end +end diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb new file mode 100644 index 0000000000..559ff5d4e6 --- /dev/null +++ b/activerecord/test/cases/arel/support/fake_record.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "date" +module FakeRecord + class Column < Struct.new(:name, :type) + end + + class Connection + attr_reader :tables + attr_accessor :visitor + + def initialize(visitor = nil) + @tables = %w{ users photos developers products} + @columns = { + "users" => [ + Column.new("id", :integer), + Column.new("name", :string), + Column.new("bool", :boolean), + Column.new("created_at", :date) + ], + "products" => [ + Column.new("id", :integer), + Column.new("price", :decimal) + ] + } + @columns_hash = { + "users" => Hash[@columns["users"].map { |x| [x.name, x] }], + "products" => Hash[@columns["products"].map { |x| [x.name, x] }] + } + @primary_keys = { + "users" => "id", + "products" => "id" + } + @visitor = visitor + end + + def columns_hash(table_name) + @columns_hash[table_name] + end + + def primary_key(name) + @primary_keys[name.to_s] + end + + def data_source_exists?(name) + @tables.include? name.to_s + end + + def columns(name, message = nil) + @columns[name.to_s] + end + + def quote_table_name(name) + "\"#{name}\"" + end + + def quote_column_name(name) + "\"#{name}\"" + end + + def schema_cache + self + end + + def quote(thing) + case thing + when DateTime + "'#{thing.strftime("%Y-%m-%d %H:%M:%S")}'" + when Date + "'#{thing.strftime("%Y-%m-%d")}'" + when true + "'t'" + when false + "'f'" + when nil + "NULL" + when Numeric + thing + else + "'#{thing.to_s.gsub("'", "\\\\'")}'" + end + end + end + + class ConnectionPool + class Spec < Struct.new(:config) + end + + attr_reader :spec, :connection + + def initialize + @spec = Spec.new(adapter: "america") + @connection = Connection.new + @connection.visitor = Arel::Visitors::ToSql.new(connection) + end + + def with_connection + yield connection + end + + def table_exists?(name) + connection.tables.include? name.to_s + end + + def columns_hash + connection.columns_hash + end + + def schema_cache + connection + end + + def quote(thing) + connection.quote thing + end + end + + class Base + attr_accessor :connection_pool + + def initialize + @connection_pool = ConnectionPool.new + end + + def connection + connection_pool.connection + end + end +end diff --git a/activerecord/test/cases/arel/table_test.rb b/activerecord/test/cases/arel/table_test.rb new file mode 100644 index 0000000000..91b7a5a480 --- /dev/null +++ b/activerecord/test/cases/arel/table_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class TableTest < Arel::Spec + before do + @relation = Table.new(:users) + end + + it "should create join nodes" do + join = @relation.create_string_join "foo" + assert_kind_of Arel::Nodes::StringJoin, join + assert_equal "foo", join.left + end + + it "should create join nodes" do + join = @relation.create_join "foo", "bar" + assert_kind_of Arel::Nodes::InnerJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a klass" do + join = @relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin + assert_kind_of Arel::Nodes::FullOuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a klass" do + join = @relation.create_join "foo", "bar", Arel::Nodes::OuterJoin + assert_kind_of Arel::Nodes::OuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should create join nodes with a klass" do + join = @relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin + assert_kind_of Arel::Nodes::RightOuterJoin, join + assert_equal "foo", join.left + assert_equal "bar", join.right + end + + it "should return an insert manager" do + im = @relation.compile_insert "VALUES(NULL)" + assert_kind_of Arel::InsertManager, im + im.into Table.new(:users) + assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql + end + + describe "skip" do + it "should add an offset" do + sm = @relation.skip 2 + sm.to_sql.must_be_like "SELECT FROM \"users\" OFFSET 2" + end + end + + describe "having" do + it "adds a having clause" do + mgr = @relation.having @relation[:id].eq(10) + mgr.to_sql.must_be_like %{ + SELECT FROM "users" HAVING "users"."id" = 10 + } + end + end + + describe "backwards compat" do + describe "join" do + it "noops on nil" do + mgr = @relation.join nil + + mgr.to_sql.must_be_like %{ SELECT FROM "users" } + end + + it "raises EmptyJoinError on empty" do + assert_raises(EmptyJoinError) do + @relation.join "" + end + end + + it "takes a second argument for join type" do + right = @relation.alias + predicate = @relation[:id].eq(right[:id]) + mgr = @relation.join(right, Nodes::OuterJoin).on(predicate) + + mgr.to_sql.must_be_like %{ + SELECT FROM "users" + LEFT OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + end + + describe "join" do + it "creates an outer join" do + right = @relation.alias + predicate = @relation[:id].eq(right[:id]) + mgr = @relation.outer_join(right).on(predicate) + + mgr.to_sql.must_be_like %{ + SELECT FROM "users" + LEFT OUTER JOIN "users" "users_2" + ON "users"."id" = "users_2"."id" + } + end + end + end + + describe "group" do + it "should create a group" do + manager = @relation.group @relation[:id] + manager.to_sql.must_be_like %{ + SELECT FROM "users" GROUP BY "users"."id" + } + end + end + + describe "alias" do + it "should create a node that proxies to a table" do + node = @relation.alias + node.name.must_equal "users_2" + node[:id].relation.must_equal node + end + end + + describe "new" do + it "should accept a hash" do + rel = Table.new :users, as: "foo" + rel.table_alias.must_equal "foo" + end + + it "ignores as if it equals name" do + rel = Table.new :users, as: "users" + rel.table_alias.must_be_nil + end + end + + describe "order" do + it "should take an order" do + manager = @relation.order "foo" + manager.to_sql.must_be_like %{ SELECT FROM "users" ORDER BY foo } + end + end + + describe "take" do + it "should add a limit" do + manager = @relation.take 1 + manager.project Nodes::SqlLiteral.new "*" + manager.to_sql.must_be_like %{ SELECT * FROM "users" LIMIT 1 } + end + end + + describe "project" do + it "can project" do + manager = @relation.project Nodes::SqlLiteral.new "*" + manager.to_sql.must_be_like %{ SELECT * FROM "users" } + end + + it "takes multiple parameters" do + manager = @relation.project Nodes::SqlLiteral.new("*"), Nodes::SqlLiteral.new("*") + manager.to_sql.must_be_like %{ SELECT *, * FROM "users" } + end + end + + describe "where" do + it "returns a tree manager" do + manager = @relation.where @relation[:id].eq 1 + manager.project @relation[:id] + manager.must_be_kind_of TreeManager + manager.to_sql.must_be_like %{ + SELECT "users"."id" + FROM "users" + WHERE "users"."id" = 1 + } + end + end + + it "should have a name" do + @relation.name.must_equal "users" + end + + it "should have a table name" do + @relation.table_name.must_equal "users" + end + + describe "[]" do + describe "when given a Symbol" do + it "manufactures an attribute if the symbol names an attribute within the relation" do + column = @relation[:id] + column.name.must_equal :id + end + end + end + + describe "equality" do + it "is equal with equal ivars" do + relation1 = Table.new(:users) + relation1.table_alias = "zomg" + relation2 = Table.new(:users) + relation2.table_alias = "zomg" + array = [relation1, relation2] + assert_equal 1, array.uniq.size + end + + it "is not equal with different ivars" do + relation1 = Table.new(:users) + relation1.table_alias = "zomg" + relation2 = Table.new(:users) + relation2.table_alias = "zomg2" + array = [relation1, relation2] + assert_equal 2, array.uniq.size + end + end + end +end diff --git a/activerecord/test/cases/arel/update_manager_test.rb b/activerecord/test/cases/arel/update_manager_test.rb new file mode 100644 index 0000000000..cc1b9ac5b3 --- /dev/null +++ b/activerecord/test/cases/arel/update_manager_test.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require_relative "helper" + +module Arel + class UpdateManagerTest < Arel::Spec + describe "new" do + it "takes an engine" do + Arel::UpdateManager.new + end + end + + it "should not quote sql literals" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.table table + um.set [[table[:name], Arel::Nodes::BindParam.new(1)]] + um.to_sql.must_be_like %{ UPDATE "users" SET "name" = ? } + end + + it "handles limit properly" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.key = "id" + um.take 10 + um.table table + um.set [[table[:name], nil]] + assert_match(/LIMIT 10/, um.to_sql) + end + + describe "set" do + it "updates with null" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.table table + um.set [[table[:name], nil]] + um.to_sql.must_be_like %{ UPDATE "users" SET "name" = NULL } + end + + it "takes a string" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.table table + um.set Nodes::SqlLiteral.new "foo = bar" + um.to_sql.must_be_like %{ UPDATE "users" SET foo = bar } + end + + it "takes a list of lists" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.table table + um.set [[table[:id], 1], [table[:name], "hello"]] + um.to_sql.must_be_like %{ + UPDATE "users" SET "id" = 1, "name" = 'hello' + } + end + + it "chains" do + table = Table.new(:users) + um = Arel::UpdateManager.new + um.set([[table[:id], 1], [table[:name], "hello"]]).must_equal um + end + end + + describe "table" do + it "generates an update statement" do + um = Arel::UpdateManager.new + um.table Table.new(:users) + um.to_sql.must_be_like %{ UPDATE "users" } + end + + it "chains" do + um = Arel::UpdateManager.new + um.table(Table.new(:users)).must_equal um + end + + it "generates an update statement with joins" do + um = Arel::UpdateManager.new + + table = Table.new(:users) + join_source = Arel::Nodes::JoinSource.new( + table, + [table.create_join(Table.new(:posts))] + ) + + um.table join_source + um.to_sql.must_be_like %{ UPDATE "users" INNER JOIN "posts" } + end + end + + describe "where" do + it "generates a where clause" do + table = Table.new :users + um = Arel::UpdateManager.new + um.table table + um.where table[:id].eq(1) + um.to_sql.must_be_like %{ + UPDATE "users" WHERE "users"."id" = 1 + } + end + + it "chains" do + table = Table.new :users + um = Arel::UpdateManager.new + um.table table + um.where(table[:id].eq(1)).must_equal um + end + end + + describe "key" do + before do + @table = Table.new :users + @um = Arel::UpdateManager.new + @um.key = @table[:foo] + end + + it "can be set" do + @um.ast.key.must_equal @table[:foo] + end + + it "can be accessed" do + @um.key.must_equal @table[:foo] + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb new file mode 100644 index 0000000000..3baccc7316 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class TestDepthFirst < Arel::Test + Collector = Struct.new(:calls) do + def call(object) + calls << object + end + end + + def setup + @collector = Collector.new [] + @visitor = Visitors::DepthFirst.new @collector + end + + def test_raises_with_object + assert_raises(TypeError) do + @visitor.accept(Object.new) + end + end + + + # unary ops + [ + Arel::Nodes::Not, + Arel::Nodes::Group, + Arel::Nodes::On, + Arel::Nodes::Grouping, + Arel::Nodes::Offset, + Arel::Nodes::Ordering, + Arel::Nodes::StringJoin, + Arel::Nodes::UnqualifiedColumn, + Arel::Nodes::Top, + Arel::Nodes::Limit, + Arel::Nodes::Else, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + op = klass.new(:a) + @visitor.accept op + assert_equal [:a, op], @collector.calls + end + end + + # functions + [ + Arel::Nodes::Exists, + Arel::Nodes::Avg, + Arel::Nodes::Min, + Arel::Nodes::Max, + Arel::Nodes::Sum, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + func = klass.new(:a, "b") + @visitor.accept func + assert_equal [:a, "b", false, func], @collector.calls + end + end + + def test_named_function + func = Arel::Nodes::NamedFunction.new(:a, :b, "c") + @visitor.accept func + assert_equal [:a, :b, false, "c", func], @collector.calls + end + + def test_lock + lock = Nodes::Lock.new true + @visitor.accept lock + assert_equal [lock], @collector.calls + end + + def test_count + count = Nodes::Count.new :a, :b, "c" + @visitor.accept count + assert_equal [:a, "c", :b, count], @collector.calls + end + + def test_inner_join + join = Nodes::InnerJoin.new :a, :b + @visitor.accept join + assert_equal [:a, :b, join], @collector.calls + end + + def test_full_outer_join + join = Nodes::FullOuterJoin.new :a, :b + @visitor.accept join + assert_equal [:a, :b, join], @collector.calls + end + + def test_outer_join + join = Nodes::OuterJoin.new :a, :b + @visitor.accept join + assert_equal [:a, :b, join], @collector.calls + end + + def test_right_outer_join + join = Nodes::RightOuterJoin.new :a, :b + @visitor.accept join + assert_equal [:a, :b, join], @collector.calls + end + + [ + Arel::Nodes::Assignment, + Arel::Nodes::Between, + Arel::Nodes::Concat, + Arel::Nodes::DoesNotMatch, + Arel::Nodes::Equality, + Arel::Nodes::GreaterThan, + Arel::Nodes::GreaterThanOrEqual, + Arel::Nodes::In, + Arel::Nodes::LessThan, + Arel::Nodes::LessThanOrEqual, + Arel::Nodes::Matches, + Arel::Nodes::NotEqual, + Arel::Nodes::NotIn, + Arel::Nodes::Or, + Arel::Nodes::TableAlias, + Arel::Nodes::Values, + Arel::Nodes::As, + Arel::Nodes::DeleteStatement, + Arel::Nodes::JoinSource, + Arel::Nodes::When, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + binary = klass.new(:a, :b) + @visitor.accept binary + assert_equal [:a, :b, binary], @collector.calls + end + end + + def test_Arel_Nodes_InfixOperation + binary = Arel::Nodes::InfixOperation.new(:o, :a, :b) + @visitor.accept binary + assert_equal [:a, :b, binary], @collector.calls + end + + # N-ary + [ + Arel::Nodes::And, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + binary = klass.new([:a, :b, :c]) + @visitor.accept binary + assert_equal [:a, :b, :c, binary], @collector.calls + end + end + + [ + Arel::Attributes::Integer, + Arel::Attributes::Float, + Arel::Attributes::String, + Arel::Attributes::Time, + Arel::Attributes::Boolean, + Arel::Attributes::Attribute + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + binary = klass.new(:a, :b) + @visitor.accept binary + assert_equal [:a, :b, binary], @collector.calls + end + end + + def test_table + relation = Arel::Table.new(:users) + @visitor.accept relation + assert_equal ["users", relation], @collector.calls + end + + def test_array + node = Nodes::Or.new(:a, :b) + list = [node] + @visitor.accept list + assert_equal [:a, :b, node, list], @collector.calls + end + + def test_set + node = Nodes::Or.new(:a, :b) + set = Set.new([node]) + @visitor.accept set + assert_equal [:a, :b, node, set], @collector.calls + end + + def test_hash + node = Nodes::Or.new(:a, :b) + hash = { node => node } + @visitor.accept hash + assert_equal [:a, :b, node, :a, :b, node, hash], @collector.calls + end + + def test_update_statement + stmt = Nodes::UpdateStatement.new + stmt.relation = :a + stmt.values << :b + stmt.wheres << :c + stmt.orders << :d + stmt.limit = :e + + @visitor.accept stmt + assert_equal [:a, :b, stmt.values, :c, stmt.wheres, :d, stmt.orders, + :e, stmt], @collector.calls + end + + def test_select_core + core = Nodes::SelectCore.new + core.projections << :a + core.froms = :b + core.wheres << :c + core.groups << :d + core.windows << :e + core.havings << :f + + @visitor.accept core + assert_equal [ + :a, core.projections, + :b, [], + core.source, + :c, core.wheres, + :d, core.groups, + :e, core.windows, + :f, core.havings, + core], @collector.calls + end + + def test_select_statement + ss = Nodes::SelectStatement.new + ss.cores.replace [:a] + ss.orders << :b + ss.limit = :c + ss.lock = :d + ss.offset = :e + + @visitor.accept ss + assert_equal [ + :a, ss.cores, + :b, ss.orders, + :c, + :d, + :e, + ss], @collector.calls + end + + def test_insert_statement + stmt = Nodes::InsertStatement.new + stmt.relation = :a + stmt.columns << :b + stmt.values = :c + + @visitor.accept stmt + assert_equal [:a, :b, stmt.columns, :c, stmt], @collector.calls + end + + def test_case + node = Arel::Nodes::Case.new + node.case = :a + node.conditions << :b + node.default = :c + + @visitor.accept node + assert_equal [:a, :b, node.conditions, :c, node], @collector.calls + end + + def test_node + node = Nodes::Node.new + @visitor.accept node + assert_equal [node], @collector.calls + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb new file mode 100644 index 0000000000..a07a1a050a --- /dev/null +++ b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "concurrent" + +module Arel + module Visitors + class DummyVisitor < Visitor + def initialize + super + @barrier = Concurrent::CyclicBarrier.new(2) + end + + def visit_Arel_Visitors_DummySuperNode(node) + 42 + end + + # This is terrible, but it's the only way to reliably reproduce + # the possible race where two threads attempt to correct the + # dispatch hash at the same time. + def send(*args) + super + rescue + # Both threads try (and fail) to dispatch to the subclass's name + @barrier.wait + raise + ensure + # Then one thread successfully completes (updating the dispatch + # table in the process) before the other finishes raising its + # exception. + Thread.current[:delay].wait if Thread.current[:delay] + end + end + + class DummySuperNode + end + + class DummySubNode < DummySuperNode + end + + class DispatchContaminationTest < Arel::Spec + before do + @connection = Table.engine.connection + @table = Table.new(:users) + end + + it "dispatches properly after failing upwards" do + node = Nodes::Union.new(Nodes::True.new, Nodes::False.new) + assert_equal "( TRUE UNION FALSE )", node.to_sql + + node.first # from Nodes::Node's Enumerable mixin + + assert_equal "( TRUE UNION FALSE )", node.to_sql + end + + it "is threadsafe when implementing superclass fallback" do + visitor = DummyVisitor.new + main_thread_finished = Concurrent::Event.new + + racing_thread = Thread.new do + Thread.current[:delay] = main_thread_finished + visitor.accept DummySubNode.new + end + + assert_equal 42, visitor.accept(DummySubNode.new) + main_thread_finished.set + + assert_equal 42, racing_thread.value + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb new file mode 100644 index 0000000000..98f3bab620 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/dot_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class TestDot < Arel::Test + def setup + @visitor = Visitors::Dot.new + end + + # functions + [ + Nodes::Sum, + Nodes::Exists, + Nodes::Max, + Nodes::Min, + Nodes::Avg, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + op = klass.new(:a, "z") + @visitor.accept op, Collectors::PlainString.new + end + end + + def test_named_function + func = Nodes::NamedFunction.new "omg", "omg" + @visitor.accept func, Collectors::PlainString.new + end + + # unary ops + [ + Arel::Nodes::Not, + Arel::Nodes::Group, + Arel::Nodes::On, + Arel::Nodes::Grouping, + Arel::Nodes::Offset, + Arel::Nodes::Ordering, + Arel::Nodes::UnqualifiedColumn, + Arel::Nodes::Top, + Arel::Nodes::Limit, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + op = klass.new(:a) + @visitor.accept op, Collectors::PlainString.new + end + end + + # binary ops + [ + Arel::Nodes::Assignment, + Arel::Nodes::Between, + Arel::Nodes::DoesNotMatch, + Arel::Nodes::Equality, + Arel::Nodes::GreaterThan, + Arel::Nodes::GreaterThanOrEqual, + Arel::Nodes::In, + Arel::Nodes::LessThan, + Arel::Nodes::LessThanOrEqual, + Arel::Nodes::Matches, + Arel::Nodes::NotEqual, + Arel::Nodes::NotIn, + Arel::Nodes::Or, + Arel::Nodes::TableAlias, + Arel::Nodes::Values, + Arel::Nodes::As, + Arel::Nodes::DeleteStatement, + Arel::Nodes::JoinSource, + Arel::Nodes::Casted, + ].each do |klass| + define_method("test_#{klass.name.gsub('::', '_')}") do + binary = klass.new(:a, :b) + @visitor.accept binary, Collectors::PlainString.new + end + end + + def test_Arel_Nodes_BindParam + node = Arel::Nodes::BindParam.new(1) + collector = Collectors::PlainString.new + assert_match '[label="<f0>Arel::Nodes::BindParam"]', @visitor.accept(node, collector).value + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb new file mode 100644 index 0000000000..7163cb34d3 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class IbmDbTest < Arel::Spec + before do + @visitor = IBM_DB.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "uses FETCH FIRST n ROWS to limit results" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(1) + sql = compile(stmt) + sql.must_be_like "SELECT FETCH FIRST 1 ROWS ONLY" + end + + it "uses FETCH FIRST n ROWS in updates with a limit" do + table = Table.new(:users) + stmt = Nodes::UpdateStatement.new + stmt.relation = table + stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1)) + stmt.key = table[:id] + sql = compile(stmt) + sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)" + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb new file mode 100644 index 0000000000..b0b031cca3 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/informix_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class InformixTest < Arel::Spec + before do + @visitor = Informix.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "uses FIRST n to limit results" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(1) + sql = compile(stmt) + sql.must_be_like "SELECT FIRST 1" + end + + it "uses FIRST n in updates with a limit" do + table = Table.new(:users) + stmt = Nodes::UpdateStatement.new + stmt.relation = table + stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1)) + stmt.key = table[:id] + sql = compile(stmt) + sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT FIRST 1 \"users\".\"id\" FROM \"users\")" + end + + it "uses SKIP n to jump results" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(10) + sql = compile(stmt) + sql.must_be_like "SELECT SKIP 10" + end + + it "uses SKIP before FIRST" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(1) + stmt.offset = Nodes::Offset.new(1) + sql = compile(stmt) + sql.must_be_like "SELECT SKIP 1 FIRST 1" + end + + it "uses INNER JOIN to perform joins" do + core = Nodes::SelectCore.new + table = Table.new(:posts) + core.source = Nodes::JoinSource.new(table, [table.create_join(Table.new(:comments))]) + + stmt = Nodes::SelectStatement.new([core]) + sql = compile(stmt) + sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"' + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb new file mode 100644 index 0000000000..340376c3d6 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/mssql_test.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class MssqlTest < Arel::Spec + before do + @visitor = MSSQL.new Table.engine.connection + @table = Arel::Table.new "users" + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "should not modify query if no offset or limit" do + stmt = Nodes::SelectStatement.new + sql = compile(stmt) + sql.must_be_like "SELECT" + end + + it "should go over table PK if no .order() or .group()" do + stmt = Nodes::SelectStatement.new + stmt.cores.first.from = @table + stmt.limit = Nodes::Limit.new(10) + sql = compile(stmt) + sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY \"users\".\"id\") as _row_num FROM \"users\") as _t WHERE _row_num BETWEEN 1 AND 10" + end + + it "caches the PK lookup for order" do + connection = Minitest::Mock.new + connection.expect(:primary_key, ["id"], ["users"]) + + # We don't care how many times these methods are called + def connection.quote_table_name(*); ""; end + def connection.quote_column_name(*); ""; end + + @visitor = MSSQL.new(connection) + stmt = Nodes::SelectStatement.new + stmt.cores.first.from = @table + stmt.limit = Nodes::Limit.new(10) + + compile(stmt) + compile(stmt) + + connection.verify + end + + it "should use TOP for limited deletes" do + stmt = Nodes::DeleteStatement.new + stmt.relation = @table + stmt.limit = Nodes::Limit.new(10) + sql = compile(stmt) + + sql.must_be_like "DELETE TOP (10) FROM \"users\"" + end + + it "should go over query ORDER BY if .order()" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.orders << Nodes::SqlLiteral.new("order_by") + sql = compile(stmt) + sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY order_by) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10" + end + + it "should go over query GROUP BY if no .order() and there is .group()" do + stmt = Nodes::SelectStatement.new + stmt.cores.first.groups << Nodes::SqlLiteral.new("group_by") + stmt.limit = Nodes::Limit.new(10) + sql = compile(stmt) + sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY group_by) as _row_num GROUP BY group_by) as _t WHERE _row_num BETWEEN 1 AND 10" + end + + it "should use BETWEEN if both .limit() and .offset" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.offset = Nodes::Offset.new(20) + sql = compile(stmt) + sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 21 AND 30" + end + + it "should use >= if only .offset" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(20) + sql = compile(stmt) + sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num >= 21" + end + + it "should generate subquery for .count" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.cores.first.projections << Nodes::Count.new("*") + sql = compile(stmt) + sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery" + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb new file mode 100644 index 0000000000..9d3bad8516 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/mysql_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class MysqlTest < Arel::Spec + before do + @visitor = MySQL.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "squashes parenthesis on multiple unions" do + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new subnode, Arel.sql("topright") + assert_equal 1, compile(node).scan("(").length + + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new Arel.sql("topleft"), subnode + assert_equal 1, compile(node).scan("(").length + end + + ### + # :'( + # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 + it "defaults limit to 18446744073709551615" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(1) + sql = compile(stmt) + sql.must_be_like "SELECT FROM DUAL LIMIT 18446744073709551615 OFFSET 1" + end + + it "should escape LIMIT" do + sc = Arel::Nodes::UpdateStatement.new + sc.relation = Table.new(:users) + sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg")) + assert_equal("UPDATE \"users\" LIMIT 'omg'", compile(sc)) + end + + it "uses DUAL for empty from" do + stmt = Nodes::SelectStatement.new + sql = compile(stmt) + sql.must_be_like "SELECT FROM DUAL" + end + + describe "locking" do + it "defaults to FOR UPDATE when locking" do + node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) + compile(node).must_be_like "FOR UPDATE" + end + + it "allows a custom string to be used as a lock" do + node = Nodes::Lock.new(Arel.sql("LOCK IN SHARE MODE")) + compile(node).must_be_like "LOCK IN SHARE MODE" + end + end + + describe "concat" do + it "concats columns" do + @table = Table.new(:users) + query = @table[:name].concat(@table[:name]) + compile(query).must_be_like %{ + CONCAT("users"."name", "users"."name") + } + end + + it "concats a string" do + @table = Table.new(:users) + query = @table[:name].concat(Nodes.build_quoted("abc")) + compile(query).must_be_like %{ + CONCAT("users"."name", 'abc') + } + end + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb new file mode 100644 index 0000000000..83a2ee36ca --- /dev/null +++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class Oracle12Test < Arel::Spec + before do + @visitor = Oracle12.new Table.engine.connection + @table = Table.new(:users) + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "modified except to be minus" do + left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10") + right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20") + sql = compile Nodes::Except.new(left, right) + sql.must_be_like %{ + ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 ) + } + end + + it "generates select options offset then limit" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(1) + stmt.limit = Nodes::Limit.new(10) + sql = compile(stmt) + sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY" + end + + describe "locking" do + it "generates ArgumentError if limit and lock are used" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE")) + assert_raises ArgumentError do + compile(stmt) + end + end + + it "defaults to FOR UPDATE when locking" do + node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) + compile(node).must_be_like "FOR UPDATE" + end + end + + describe "Nodes::BindParam" do + it "increments each bind param" do + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) + compile(query).must_be_like %{ + "users"."name" = :a1 AND "users"."id" = :a2 + } + end + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb new file mode 100644 index 0000000000..e1dfe40cf9 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/oracle_test.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class OracleTest < Arel::Spec + before do + @visitor = Oracle.new Table.engine.connection + @table = Table.new(:users) + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "modifies order when there is distinct and first value" do + # *sigh* + select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" + stmt = Nodes::SelectStatement.new + stmt.cores.first.projections << Nodes::SqlLiteral.new(select) + stmt.orders << Nodes::SqlLiteral.new("foo") + sql = compile(stmt) + sql.must_be_like %{ + SELECT #{select} ORDER BY alias_0__ + } + end + + it "is idempotent with crazy query" do + # *sigh* + select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" + stmt = Nodes::SelectStatement.new + stmt.cores.first.projections << Nodes::SqlLiteral.new(select) + stmt.orders << Nodes::SqlLiteral.new("foo") + + sql = compile(stmt) + sql2 = compile(stmt) + sql.must_equal sql2 + end + + it "splits orders with commas" do + # *sigh* + select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" + stmt = Nodes::SelectStatement.new + stmt.cores.first.projections << Nodes::SqlLiteral.new(select) + stmt.orders << Nodes::SqlLiteral.new("foo, bar") + sql = compile(stmt) + sql.must_be_like %{ + SELECT #{select} ORDER BY alias_0__, alias_1__ + } + end + + it "splits orders with commas and function calls" do + # *sigh* + select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" + stmt = Nodes::SelectStatement.new + stmt.cores.first.projections << Nodes::SqlLiteral.new(select) + stmt.orders << Nodes::SqlLiteral.new("NVL(LOWER(bar, foo), foo) DESC, UPPER(baz)") + sql = compile(stmt) + sql.must_be_like %{ + SELECT #{select} ORDER BY alias_0__ DESC, alias_1__ + } + end + + describe "Nodes::SelectStatement" do + describe "limit" do + it "adds a rownum clause" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + sql = compile stmt + sql.must_be_like %{ SELECT WHERE ROWNUM <= 10 } + end + + it "is idempotent" do + stmt = Nodes::SelectStatement.new + stmt.orders << Nodes::SqlLiteral.new("foo") + stmt.limit = Nodes::Limit.new(10) + sql = compile stmt + sql2 = compile stmt + sql.must_equal sql2 + end + + it "creates a subquery when there is order_by" do + stmt = Nodes::SelectStatement.new + stmt.orders << Nodes::SqlLiteral.new("foo") + stmt.limit = Nodes::Limit.new(10) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM (SELECT ORDER BY foo ) WHERE ROWNUM <= 10 + } + end + + it "creates a subquery when there is group by" do + stmt = Nodes::SelectStatement.new + stmt.cores.first.groups << Nodes::SqlLiteral.new("foo") + stmt.limit = Nodes::Limit.new(10) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM (SELECT GROUP BY foo ) WHERE ROWNUM <= 10 + } + end + + it "creates a subquery when there is DISTINCT" do + stmt = Nodes::SelectStatement.new + stmt.cores.first.set_quantifier = Arel::Nodes::Distinct.new + stmt.cores.first.projections << Nodes::SqlLiteral.new("id") + stmt.limit = Arel::Nodes::Limit.new(10) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM (SELECT DISTINCT id ) WHERE ROWNUM <= 10 + } + end + + it "creates a different subquery when there is an offset" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.offset = Nodes::Offset.new(10) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (SELECT ) raw_sql_ + WHERE rownum <= 20 + ) + WHERE raw_rnum_ > 10 + } + end + + it "creates a subquery when there is limit and offset with BindParams" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1)) + stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1)) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (SELECT ) raw_sql_ + WHERE rownum <= (:a1 + :a2) + ) + WHERE raw_rnum_ > :a3 + } + end + + it "is idempotent with different subquery" do + stmt = Nodes::SelectStatement.new + stmt.limit = Nodes::Limit.new(10) + stmt.offset = Nodes::Offset.new(10) + sql = compile stmt + sql2 = compile stmt + sql.must_equal sql2 + end + end + + describe "only offset" do + it "creates a select from subquery with rownum condition" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(10) + sql = compile stmt + sql.must_be_like %{ + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (SELECT) raw_sql_ + ) + WHERE raw_rnum_ > 10 + } + end + end + end + + it "modified except to be minus" do + left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10") + right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20") + sql = compile Nodes::Except.new(left, right) + sql.must_be_like %{ + ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 ) + } + end + + describe "locking" do + it "defaults to FOR UPDATE when locking" do + node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) + compile(node).must_be_like "FOR UPDATE" + end + end + + describe "Nodes::BindParam" do + it "increments each bind param" do + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) + compile(query).must_be_like %{ + "users"."name" = :a1 AND "users"."id" = :a2 + } + end + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb new file mode 100644 index 0000000000..f7f2c76b6f --- /dev/null +++ b/activerecord/test/cases/arel/visitors/postgres_test.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class PostgresTest < Arel::Spec + before do + @visitor = PostgreSQL.new Table.engine.connection + @table = Table.new(:users) + @attr = @table[:id] + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + describe "locking" do + it "defaults to FOR UPDATE" do + compile(Nodes::Lock.new(Arel.sql("FOR UPDATE"))).must_be_like %{ + FOR UPDATE + } + end + + it "allows a custom string to be used as a lock" do + node = Nodes::Lock.new(Arel.sql("FOR SHARE")) + compile(node).must_be_like %{ + FOR SHARE + } + end + end + + it "should escape LIMIT" do + sc = Arel::Nodes::SelectStatement.new + sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg")) + sc.cores.first.projections << Arel.sql("DISTINCT ON") + sc.orders << Arel.sql("xyz") + sql = compile(sc) + assert_match(/LIMIT 'omg'/, sql) + assert_equal 1, sql.scan(/LIMIT/).length, "should have one limit" + end + + it "should support DISTINCT ON" do + core = Arel::Nodes::SelectCore.new + core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron")) + assert_match "DISTINCT ON ( aaron )", compile(core) + end + + it "should support DISTINCT" do + core = Arel::Nodes::SelectCore.new + core.set_quantifier = Arel::Nodes::Distinct.new + assert_equal "SELECT DISTINCT", compile(core) + end + + it "encloses LATERAL queries in parens" do + subquery = @table.project(:id).where(@table[:name].matches("foo%")) + compile(subquery.lateral).must_be_like %{ + LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') + } + end + + it "produces LATERAL queries with alias" do + subquery = @table.project(:id).where(@table[:name].matches("foo%")) + compile(subquery.lateral("bar")).must_be_like %{ + LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') bar + } + end + + describe "Nodes::Matches" do + it "should know how to visit" do + node = @table[:name].matches("foo%") + node.must_be_kind_of Nodes::Matches + node.case_sensitive.must_equal(false) + compile(node).must_be_like %{ + "users"."name" ILIKE 'foo%' + } + end + + it "should know how to visit case sensitive" do + node = @table[:name].matches("foo%", nil, true) + node.case_sensitive.must_equal(true) + compile(node).must_be_like %{ + "users"."name" LIKE 'foo%' + } + end + + it "can handle ESCAPE" do + node = @table[:name].matches("foo!%", "!") + compile(node).must_be_like %{ + "users"."name" ILIKE 'foo!%' ESCAPE '!' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].matches("foo%")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') + } + end + end + + describe "Nodes::DoesNotMatch" do + it "should know how to visit" do + node = @table[:name].does_not_match("foo%") + node.must_be_kind_of Nodes::DoesNotMatch + node.case_sensitive.must_equal(false) + compile(node).must_be_like %{ + "users"."name" NOT ILIKE 'foo%' + } + end + + it "should know how to visit case sensitive" do + node = @table[:name].does_not_match("foo%", nil, true) + node.case_sensitive.must_equal(true) + compile(node).must_be_like %{ + "users"."name" NOT LIKE 'foo%' + } + end + + it "can handle ESCAPE" do + node = @table[:name].does_not_match("foo!%", "!") + compile(node).must_be_like %{ + "users"."name" NOT ILIKE 'foo!%' ESCAPE '!' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].does_not_match("foo%")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT ILIKE 'foo%') + } + end + end + + describe "Nodes::Regexp" do + it "should know how to visit" do + node = @table[:name].matches_regexp("foo.*") + node.must_be_kind_of Nodes::Regexp + node.case_sensitive.must_equal(true) + compile(node).must_be_like %{ + "users"."name" ~ 'foo.*' + } + end + + it "can handle case insensitive" do + node = @table[:name].matches_regexp("foo.*", false) + node.must_be_kind_of Nodes::Regexp + node.case_sensitive.must_equal(false) + compile(node).must_be_like %{ + "users"."name" ~* 'foo.*' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].matches_regexp("foo.*")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ~ 'foo.*') + } + end + end + + describe "Nodes::NotRegexp" do + it "should know how to visit" do + node = @table[:name].does_not_match_regexp("foo.*") + node.must_be_kind_of Nodes::NotRegexp + node.case_sensitive.must_equal(true) + compile(node).must_be_like %{ + "users"."name" !~ 'foo.*' + } + end + + it "can handle case insensitive" do + node = @table[:name].does_not_match_regexp("foo.*", false) + node.case_sensitive.must_equal(false) + compile(node).must_be_like %{ + "users"."name" !~* 'foo.*' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].does_not_match_regexp("foo.*")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" !~ 'foo.*') + } + end + end + + describe "Nodes::BindParam" do + it "increments each bind param" do + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) + compile(query).must_be_like %{ + "users"."name" = $1 AND "users"."id" = $2 + } + end + end + + describe "Nodes::Cube" do + it "should know how to visit with array arguments" do + node = Arel::Nodes::Cube.new([@table[:name], @table[:bool]]) + compile(node).must_be_like %{ + CUBE( "users"."name", "users"."bool" ) + } + end + + it "should know how to visit with CubeDimension Argument" do + dimensions = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]]) + node = Arel::Nodes::Cube.new(dimensions) + compile(node).must_be_like %{ + CUBE( "users"."name", "users"."bool" ) + } + end + + it "should know how to generate parenthesis when supplied with many Dimensions" do + dim1 = Arel::Nodes::GroupingElement.new(@table[:name]) + dim2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) + node = Arel::Nodes::Cube.new([dim1, dim2]) + compile(node).must_be_like %{ + CUBE( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) ) + } + end + end + + describe "Nodes::GroupingSet" do + it "should know how to visit with array arguments" do + node = Arel::Nodes::GroupingSet.new([@table[:name], @table[:bool]]) + compile(node).must_be_like %{ + GROUPING SETS( "users"."name", "users"."bool" ) + } + end + + it "should know how to visit with CubeDimension Argument" do + group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]]) + node = Arel::Nodes::GroupingSet.new(group) + compile(node).must_be_like %{ + GROUPING SETS( "users"."name", "users"."bool" ) + } + end + + it "should know how to generate parenthesis when supplied with many Dimensions" do + group1 = Arel::Nodes::GroupingElement.new(@table[:name]) + group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) + node = Arel::Nodes::GroupingSet.new([group1, group2]) + compile(node).must_be_like %{ + GROUPING SETS( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) ) + } + end + end + + describe "Nodes::RollUp" do + it "should know how to visit with array arguments" do + node = Arel::Nodes::RollUp.new([@table[:name], @table[:bool]]) + compile(node).must_be_like %{ + ROLLUP( "users"."name", "users"."bool" ) + } + end + + it "should know how to visit with CubeDimension Argument" do + group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]]) + node = Arel::Nodes::RollUp.new(group) + compile(node).must_be_like %{ + ROLLUP( "users"."name", "users"."bool" ) + } + end + + it "should know how to generate parenthesis when supplied with many Dimensions" do + group1 = Arel::Nodes::GroupingElement.new(@table[:name]) + group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) + node = Arel::Nodes::RollUp.new([group1, group2]) + compile(node).must_be_like %{ + ROLLUP( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) ) + } + end + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb new file mode 100644 index 0000000000..6650b6ff3a --- /dev/null +++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "../helper" + +module Arel + module Visitors + class SqliteTest < Arel::Spec + before do + @visitor = SQLite.new Table.engine.connection_pool + end + + it "defaults limit to -1" do + stmt = Nodes::SelectStatement.new + stmt.offset = Nodes::Offset.new(1) + sql = @visitor.accept(stmt, Collectors::SQLString.new).value + sql.must_be_like "SELECT LIMIT -1 OFFSET 1" + end + + it "does not support locking" do + node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) + assert_equal "", @visitor.accept(node, Collectors::SQLString.new).value + end + + it "does not support boolean" do + node = Nodes::True.new() + assert_equal "1", @visitor.accept(node, Collectors::SQLString.new).value + node = Nodes::False.new() + assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb new file mode 100644 index 0000000000..e8ac50bfa3 --- /dev/null +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -0,0 +1,654 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "bigdecimal" + +module Arel + module Visitors + describe "the to_sql visitor" do + before do + @conn = FakeRecord::Base.new + @visitor = ToSql.new @conn.connection + @table = Table.new(:users) + @attr = @table[:id] + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value + end + + it "works with BindParams" do + node = Nodes::BindParam.new(1) + sql = compile node + sql.must_be_like "?" + end + + it "does not quote BindParams used as part of a Values" do + bp = Nodes::BindParam.new(1) + values = Nodes::Values.new([bp]) + sql = compile values + sql.must_be_like "VALUES (?)" + end + + it "can define a dispatch method" do + visited = false + viz = Class.new(Arel::Visitors::Visitor) { + define_method(:hello) do |node, c| + visited = true + end + + def dispatch + { Arel::Table => "hello" } + end + }.new + + viz.accept(@table, Collectors::SQLString.new) + assert visited, "hello method was called" + end + + it "should not quote sql literals" do + node = @table[Arel.star] + sql = compile node + sql.must_be_like '"users".*' + end + + it "should visit named functions" do + function = Nodes::NamedFunction.new("omg", [Arel.star]) + assert_equal "omg(*)", compile(function) + end + + it "should chain predications on named functions" do + function = Nodes::NamedFunction.new("omg", [Arel.star]) + sql = compile(function.eq(2)) + sql.must_be_like %{ omg(*) = 2 } + end + + it "should handle nil with named functions" do + function = Nodes::NamedFunction.new("omg", [Arel.star]) + sql = compile(function.eq(nil)) + sql.must_be_like %{ omg(*) IS NULL } + end + + it "should visit built-in functions" do + function = Nodes::Count.new([Arel.star]) + assert_equal "COUNT(*)", compile(function) + + function = Nodes::Sum.new([Arel.star]) + assert_equal "SUM(*)", compile(function) + + function = Nodes::Max.new([Arel.star]) + assert_equal "MAX(*)", compile(function) + + function = Nodes::Min.new([Arel.star]) + assert_equal "MIN(*)", compile(function) + + function = Nodes::Avg.new([Arel.star]) + assert_equal "AVG(*)", compile(function) + end + + it "should visit built-in functions operating on distinct values" do + function = Nodes::Count.new([Arel.star]) + function.distinct = true + assert_equal "COUNT(DISTINCT *)", compile(function) + + function = Nodes::Sum.new([Arel.star]) + function.distinct = true + assert_equal "SUM(DISTINCT *)", compile(function) + + function = Nodes::Max.new([Arel.star]) + function.distinct = true + assert_equal "MAX(DISTINCT *)", compile(function) + + function = Nodes::Min.new([Arel.star]) + function.distinct = true + assert_equal "MIN(DISTINCT *)", compile(function) + + function = Nodes::Avg.new([Arel.star]) + function.distinct = true + assert_equal "AVG(DISTINCT *)", compile(function) + end + + it "works with lists" do + function = Nodes::NamedFunction.new("omg", [Arel.star, Arel.star]) + assert_equal "omg(*, *)", compile(function) + end + + describe "Nodes::Equality" do + it "should escape strings" do + test = Table.new(:users)[:name].eq "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" = 'Aaron Patterson' + } + end + + it "should handle false" do + table = Table.new(:users) + val = Nodes.build_quoted(false, table[:active]) + sql = compile Nodes::Equality.new(val, val) + sql.must_be_like %{ 'f' = 'f' } + end + + it "should handle nil" do + sql = compile Nodes::Equality.new(@table[:name], nil) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::Grouping" do + it "wraps nested groupings in brackets only once" do + sql = compile Nodes::Grouping.new(Nodes::Grouping.new(Nodes.build_quoted("foo"))) + sql.must_equal "('foo')" + end + end + + describe "Nodes::NotEqual" do + it "should handle false" do + val = Nodes.build_quoted(false, @table[:active]) + sql = compile Nodes::NotEqual.new(@table[:active], val) + sql.must_be_like %{ "users"."active" != 'f' } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::NotEqual.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end + + it "should visit string subclass" do + [ + Class.new(String).new(":'("), + Class.new(Class.new(String)).new(":'("), + ].each do |obj| + val = Nodes.build_quoted(obj, @table[:active]) + sql = compile Nodes::NotEqual.new(@table[:name], val) + sql.must_be_like %{ "users"."name" != ':\\'(' } + end + end + + it "should visit_Class" do + compile(Nodes.build_quoted(DateTime)).must_equal "'DateTime'" + end + + it "should escape LIMIT" do + sc = Arel::Nodes::SelectStatement.new + sc.limit = Arel::Nodes::Limit.new(Nodes.build_quoted("omg")) + assert_match(/LIMIT 'omg'/, compile(sc)) + end + + it "should contain a single space before ORDER BY" do + table = Table.new(:users) + test = table.order(table[:name]) + sql = compile test + assert_match(/"users" ORDER BY/, sql) + end + + it "should quote LIMIT without column type coercion" do + table = Table.new(:users) + sc = table.where(table[:name].eq(0)).take(1).ast + assert_match(/WHERE "users"."name" = 0 LIMIT 1/, compile(sc)) + end + + it "should visit_DateTime" do + dt = DateTime.now + table = Table.new(:users) + test = table[:created_at].eq dt + sql = compile test + + sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d %H:%M:%S")}'} + end + + it "should visit_Float" do + test = Table.new(:products)[:price].eq 2.14 + sql = compile test + sql.must_be_like %{"products"."price" = 2.14} + end + + it "should visit_Not" do + sql = compile Nodes::Not.new(Arel.sql("foo")) + sql.must_be_like "NOT (foo)" + end + + it "should apply Not to the whole expression" do + node = Nodes::And.new [@attr.eq(10), @attr.eq(11)] + sql = compile Nodes::Not.new(node) + sql.must_be_like %{NOT ("users"."id" = 10 AND "users"."id" = 11)} + end + + it "should visit_As" do + as = Nodes::As.new(Arel.sql("foo"), Arel.sql("bar")) + sql = compile as + sql.must_be_like "foo AS bar" + end + + it "should visit_Bignum" do + compile 8787878092 + end + + it "should visit_Hash" do + compile(Nodes.build_quoted(a: 1)) + end + + it "should visit_Set" do + compile Nodes.build_quoted(Set.new([1, 2])) + end + + it "should visit_BigDecimal" do + compile Nodes.build_quoted(BigDecimal("2.14")) + end + + it "should visit_Date" do + dt = Date.today + table = Table.new(:users) + test = table[:created_at].eq dt + sql = compile test + + sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d")}'} + end + + it "should visit_NilClass" do + compile(Nodes.build_quoted(nil)).must_be_like "NULL" + end + + it "unsupported input should raise UnsupportedVisitError" do + error = assert_raises(UnsupportedVisitError) { compile(nil) } + assert_match(/\AUnsupported/, error.message) + end + + it "should visit_Arel_SelectManager, which is a subquery" do + mgr = Table.new(:foo).project(:bar) + compile(mgr).must_be_like '(SELECT bar FROM "foo")' + end + + it "should visit_Arel_Nodes_And" do + node = Nodes::And.new [@attr.eq(10), @attr.eq(11)] + compile(node).must_be_like %{ + "users"."id" = 10 AND "users"."id" = 11 + } + end + + it "should visit_Arel_Nodes_Or" do + node = Nodes::Or.new @attr.eq(10), @attr.eq(11) + compile(node).must_be_like %{ + "users"."id" = 10 OR "users"."id" = 11 + } + end + + it "should visit_Arel_Nodes_Assignment" do + column = @table["id"] + node = Nodes::Assignment.new( + Nodes::UnqualifiedColumn.new(column), + Nodes::UnqualifiedColumn.new(column) + ) + compile(node).must_be_like %{ + "id" = "id" + } + end + + it "should visit visit_Arel_Attributes_Time" do + attr = Attributes::Time.new(@attr.relation, @attr.name) + compile attr + end + + it "should visit_TrueClass" do + test = Table.new(:users)[:bool].eq(true) + compile(test).must_be_like %{ "users"."bool" = 't' } + end + + describe "Nodes::Matches" do + it "should know how to visit" do + node = @table[:name].matches("foo%") + compile(node).must_be_like %{ + "users"."name" LIKE 'foo%' + } + end + + it "can handle ESCAPE" do + node = @table[:name].matches("foo!%", "!") + compile(node).must_be_like %{ + "users"."name" LIKE 'foo!%' ESCAPE '!' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].matches("foo%")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" LIKE 'foo%') + } + end + end + + describe "Nodes::DoesNotMatch" do + it "should know how to visit" do + node = @table[:name].does_not_match("foo%") + compile(node).must_be_like %{ + "users"."name" NOT LIKE 'foo%' + } + end + + it "can handle ESCAPE" do + node = @table[:name].does_not_match("foo!%", "!") + compile(node).must_be_like %{ + "users"."name" NOT LIKE 'foo!%' ESCAPE '!' + } + end + + it "can handle subqueries" do + subquery = @table.project(:id).where(@table[:name].does_not_match("foo%")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT LIKE 'foo%') + } + end + end + + describe "Nodes::Ordering" do + it "should know how to visit" do + node = @attr.desc + compile(node).must_be_like %{ + "users"."id" DESC + } + end + end + + describe "Nodes::In" do + it "should know how to visit" do + node = @attr.in [1, 2, 3] + compile(node).must_be_like %{ + "users"."id" IN (1, 2, 3) + } + end + + it "should return 1=0 when empty right which is always false" do + node = @attr.in [] + compile(node).must_equal "1=0" + end + + it "can handle two dot ranges" do + node = @attr.between 1..3 + compile(node).must_be_like %{ + "users"."id" BETWEEN 1 AND 3 + } + end + + it "can handle three dot ranges" do + node = @attr.between 1...3 + compile(node).must_be_like %{ + "users"."id" >= 1 AND "users"."id" < 3 + } + end + + it "can handle ranges bounded by infinity" do + node = @attr.between 1..Float::INFINITY + compile(node).must_be_like %{ + "users"."id" >= 1 + } + node = @attr.between(-Float::INFINITY..3) + compile(node).must_be_like %{ + "users"."id" <= 3 + } + node = @attr.between(-Float::INFINITY...3) + compile(node).must_be_like %{ + "users"."id" < 3 + } + node = @attr.between(-Float::INFINITY..Float::INFINITY) + compile(node).must_be_like %{1=1} + end + + it "can handle subqueries" do + table = Table.new(:users) + subquery = table.project(:id).where(table[:name].eq("Aaron")) + node = @attr.in subquery + compile(node).must_be_like %{ + "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron') + } + end + end + + describe "Nodes::InfixOperation" do + it "should handle Multiplication" do + node = Arel::Attributes::Decimal.new(Table.new(:products), :price) * Arel::Attributes::Decimal.new(Table.new(:currency_rates), :rate) + compile(node).must_equal %("products"."price" * "currency_rates"."rate") + end + + it "should handle Division" do + node = Arel::Attributes::Decimal.new(Table.new(:products), :price) / 5 + compile(node).must_equal %("products"."price" / 5) + end + + it "should handle Addition" do + node = Arel::Attributes::Decimal.new(Table.new(:products), :price) + 6 + compile(node).must_equal %(("products"."price" + 6)) + end + + it "should handle Subtraction" do + node = Arel::Attributes::Decimal.new(Table.new(:products), :price) - 7 + compile(node).must_equal %(("products"."price" - 7)) + end + + it "should handle Concatenation" do + table = Table.new(:users) + node = table[:name].concat(table[:name]) + compile(node).must_equal %("users"."name" || "users"."name") + end + + it "should handle BitwiseAnd" do + node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) & 16 + compile(node).must_equal %(("products"."bitmap" & 16)) + end + + it "should handle BitwiseOr" do + node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) | 16 + compile(node).must_equal %(("products"."bitmap" | 16)) + end + + it "should handle BitwiseXor" do + node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) ^ 16 + compile(node).must_equal %(("products"."bitmap" ^ 16)) + end + + it "should handle BitwiseShiftLeft" do + node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) << 4 + compile(node).must_equal %(("products"."bitmap" << 4)) + end + + it "should handle BitwiseShiftRight" do + node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) >> 4 + compile(node).must_equal %(("products"."bitmap" >> 4)) + end + + it "should handle arbitrary operators" do + node = Arel::Nodes::InfixOperation.new( + "&&", + Arel::Attributes::String.new(Table.new(:products), :name), + Arel::Attributes::String.new(Table.new(:products), :name) + ) + compile(node).must_equal %("products"."name" && "products"."name") + end + end + + describe "Nodes::UnaryOperation" do + it "should handle BitwiseNot" do + node = ~ Arel::Attributes::Integer.new(Table.new(:products), :bitmap) + compile(node).must_equal %( ~ "products"."bitmap") + end + + it "should handle arbitrary operators" do + node = Arel::Nodes::UnaryOperation.new("!", Arel::Attributes::String.new(Table.new(:products), :active)) + compile(node).must_equal %( ! "products"."active") + end + end + + describe "Nodes::NotIn" do + it "should know how to visit" do + node = @attr.not_in [1, 2, 3] + compile(node).must_be_like %{ + "users"."id" NOT IN (1, 2, 3) + } + end + + it "should return 1=1 when empty right which is always true" do + node = @attr.not_in [] + compile(node).must_equal "1=1" + end + + it "can handle two dot ranges" do + node = @attr.not_between 1..3 + compile(node).must_equal( + %{("users"."id" < 1 OR "users"."id" > 3)} + ) + end + + it "can handle three dot ranges" do + node = @attr.not_between 1...3 + compile(node).must_equal( + %{("users"."id" < 1 OR "users"."id" >= 3)} + ) + end + + it "can handle ranges bounded by infinity" do + node = @attr.not_between 1..Float::INFINITY + compile(node).must_be_like %{ + "users"."id" < 1 + } + node = @attr.not_between(-Float::INFINITY..3) + compile(node).must_be_like %{ + "users"."id" > 3 + } + node = @attr.not_between(-Float::INFINITY...3) + compile(node).must_be_like %{ + "users"."id" >= 3 + } + node = @attr.not_between(-Float::INFINITY..Float::INFINITY) + compile(node).must_be_like %{1=0} + end + + it "can handle subqueries" do + table = Table.new(:users) + subquery = table.project(:id).where(table[:name].eq("Aaron")) + node = @attr.not_in subquery + compile(node).must_be_like %{ + "users"."id" NOT IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron') + } + end + end + + describe "Constants" do + it "should handle true" do + test = Table.new(:users).create_true + compile(test).must_be_like %{ + TRUE + } + end + + it "should handle false" do + test = Table.new(:users).create_false + compile(test).must_be_like %{ + FALSE + } + end + end + + describe "TableAlias" do + it "should use the underlying table for checking columns" do + test = Table.new(:users).alias("zomgusers")[:id].eq "3" + compile(test).must_be_like %{ + "zomgusers"."id" = '3' + } + end + end + + describe "distinct on" do + it "raises not implemented error" do + core = Arel::Nodes::SelectCore.new + core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron")) + + assert_raises(NotImplementedError) do + compile(core) + end + end + end + + describe "Nodes::Regexp" do + it "raises not implemented error" do + node = Arel::Nodes::Regexp.new(@table[:name], Nodes.build_quoted("foo%")) + + assert_raises(NotImplementedError) do + compile(node) + end + end + end + + describe "Nodes::NotRegexp" do + it "raises not implemented error" do + node = Arel::Nodes::NotRegexp.new(@table[:name], Nodes.build_quoted("foo%")) + + assert_raises(NotImplementedError) do + compile(node) + end + end + end + + describe "Nodes::Case" do + it "supports simple case expressions" do + node = Arel::Nodes::Case.new(@table[:name]) + .when("foo").then(1) + .else(0) + + compile(node).must_be_like %{ + CASE "users"."name" WHEN 'foo' THEN 1 ELSE 0 END + } + end + + it "supports extended case expressions" do + node = Arel::Nodes::Case.new + .when(@table[:name].in(%w(foo bar))).then(1) + .else(0) + + compile(node).must_be_like %{ + CASE WHEN "users"."name" IN ('foo', 'bar') THEN 1 ELSE 0 END + } + end + + it "works without default branch" do + node = Arel::Nodes::Case.new(@table[:name]) + .when("foo").then(1) + + compile(node).must_be_like %{ + CASE "users"."name" WHEN 'foo' THEN 1 END + } + end + + it "allows chaining multiple conditions" do + node = Arel::Nodes::Case.new(@table[:name]) + .when("foo").then(1) + .when("bar").then(2) + .else(0) + + compile(node).must_be_like %{ + CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 2 ELSE 0 END + } + end + + it "supports #when with two arguments and no #then" do + node = Arel::Nodes::Case.new @table[:name] + + { foo: 1, bar: 0 }.reduce(node) { |_node, pair| _node.when(*pair) } + + compile(node).must_be_like %{ + CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 0 END + } + end + + it "can be chained as a predicate" do + node = @table[:name].when("foo").then("bar").else("baz") + + compile(node).must_be_like %{ + CASE "users"."name" WHEN 'foo' THEN 'bar' ELSE 'baz' END + } + end + end + end + end +end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 0f7a249bf3..0cc4ed7127 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -37,6 +37,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_assigning_belongs_to_on_destroyed_object + client = Client.create!(name: "Client") + client.destroy! + assert_raise(frozen_error_class) { client.firm = nil } + assert_raise(frozen_error_class) { client.firm = Firm.new(name: "Firm") } + end + + def test_eager_loading_wont_mutate_owner_record + client = Client.eager_load(:firm_with_basic_id).first + assert_not_predicate client, :firm_id_came_from_user? + + client = Client.preload(:firm_with_basic_id).first + assert_not_predicate client, :firm_id_came_from_user? + end + def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm } end @@ -79,7 +94,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert account.valid? + assert_predicate account, :valid? ensure ActiveRecord::Base.belongs_to_required_by_default = original_value end @@ -95,7 +110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert_not account.valid? + assert_not_predicate account, :valid? assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -112,7 +127,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert_not account.valid? + assert_not_predicate account, :valid? assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -246,14 +261,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first - assert citibank_result.association(:firm_with_primary_key).loaded? + assert_predicate citibank_result.association(:firm_with_primary_key), :loaded? end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first - assert citibank_result.association(:firm_with_primary_key_symbols).loaded? + assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded? end def test_creating_the_belonging_object @@ -265,6 +280,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal apple, citibank.firm end + def test_creating_the_belonging_object_from_new_record + citibank = Account.new("credit_limit" => 10) + apple = citibank.create_firm("name" => "Apple") + assert_equal apple, citibank.firm + citibank.save + citibank.reload + assert_equal apple, citibank.firm + end + def test_creating_the_belonging_object_with_primary_key client = Client.create(name: "Primary key client") apple = client.create_firm_with_primary_key("name" => "Apple") @@ -320,7 +344,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create!(name: "Jimmy") account = client.create_account!(credit_limit: 10) assert_equal account, client.account - assert account.persisted? + assert_predicate account, :persisted? client.save client.reload assert_equal account, client.account @@ -330,7 +354,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create!(name: "Jimmy") assert_raise(ActiveRecord::RecordInvalid) { client.create_account! } assert_not_nil client.account - assert client.account.new_record? + assert_predicate client.account, :new_record? end def test_reloading_the_belonging_object @@ -442,7 +466,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_with_primary_key_counter debate = Topic.create("title" => "debate") debate2 = Topic.create("title" => "debate2") - reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate") + reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate2") + + assert_equal 0, debate.reload.replies_count + assert_equal 1, debate2.reload.replies_count + + reply.parent_title = "debate" + reply.save! + + assert_equal 1, debate.reload.replies_count + assert_equal 0, debate2.reload.replies_count + + assert_no_queries do + reply.topic_with_primary_key = debate + end assert_equal 1, debate.reload.replies_count assert_equal 0, debate2.reload.replies_count @@ -520,6 +557,48 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic.id)[:replies_count] end + def test_belongs_to_counter_after_touch + topic = Topic.create!(title: "topic") + + assert_equal 0, topic.replies_count + assert_equal 0, topic.after_touch_called + + reply = Reply.create!(title: "blah!", content: "world around!", topic_with_primary_key: topic) + + assert_equal 1, topic.replies_count + assert_equal 1, topic.after_touch_called + + reply.destroy! + + assert_equal 0, topic.replies_count + assert_equal 2, topic.after_touch_called + end + + def test_belongs_to_touch_with_reassigning + debate = Topic.create!(title: "debate") + debate2 = Topic.create!(title: "debate2") + reply = Reply.create!(title: "blah!", content: "world around!", parent_title: "debate2") + + time = 1.day.ago + + debate.touch(time: time) + debate2.touch(time: time) + + reply.parent_title = "debate" + reply.save! + + assert_operator debate.reload.updated_at, :>, time + assert_operator debate2.reload.updated_at, :>, time + + debate.touch(time: time) + debate2.touch(time: time) + + reply.topic_with_primary_key = debate2 + + assert_operator debate.reload.updated_at, :>, time + assert_operator debate2.reload.updated_at, :>, time + end + def test_belongs_to_with_touch_option_on_touch line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -627,10 +706,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm = firm - assert !final_cut.persisted? + assert_not_predicate final_cut, :persisted? assert final_cut.save - assert final_cut.persisted? - assert firm.persisted? + assert_predicate final_cut, :persisted? + assert_predicate firm, :persisted? assert_equal firm, final_cut.firm final_cut.association(:firm).reload assert_equal firm, final_cut.firm @@ -640,10 +719,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm_with_primary_key = firm - assert !final_cut.persisted? + assert_not_predicate final_cut, :persisted? assert final_cut.save - assert final_cut.persisted? - assert firm.persisted? + assert_predicate final_cut, :persisted? + assert_predicate firm, :persisted? assert_equal firm, final_cut.firm_with_primary_key final_cut.association(:firm_with_primary_key).reload assert_equal firm, final_cut.firm_with_primary_key @@ -790,7 +869,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_cant_save_readonly_association assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! } - assert companies(:first_client).readonly_firm.readonly? + assert_predicate companies(:first_client).readonly_firm, :readonly? end def test_polymorphic_assignment_foreign_key_type_string @@ -931,6 +1010,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify" end + class DestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy + end + + class UndestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "DestroyableBook", foreign_key: "author_id" + before_destroy :dont + + def dont + throw(:abort) + end + end + + def test_dependency_should_halt_parent_destruction + author = UndestroyableAuthor.create!(name: "Test") + book = DestroyableBook.create!(author: author) + + assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do + assert_not book.destroy + end + end + def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause new_firm = accounts(:signals37).build_firm(name: "Apple") assert_equal new_firm.name, "Apple" @@ -949,15 +1052,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase firm_proxy = client.send(:association_instance_get, :firm) firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition) - assert !firm_proxy.stale_target? - assert !firm_with_condition_proxy.stale_target? + assert_not_predicate firm_proxy, :stale_target? + assert_not_predicate firm_with_condition_proxy, :stale_target? assert_equal companies(:first_firm), client.firm assert_equal companies(:first_firm), client.firm_with_condition client.client_of = companies(:another_firm).id - assert firm_proxy.stale_target? - assert firm_with_condition_proxy.stale_target? + assert_predicate firm_proxy, :stale_target? + assert_predicate firm_with_condition_proxy, :stale_target? assert_equal companies(:another_firm), client.firm assert_equal companies(:another_firm), client.firm_with_condition end @@ -968,12 +1071,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable proxy = sponsor.send(:association_instance_get, :sponsorable) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal members(:groucho), sponsor.sponsorable sponsor.sponsorable_id = members(:some_other_guy).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal members(:some_other_guy), sponsor.sponsorable end @@ -983,12 +1086,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable proxy = sponsor.send(:association_instance_get, :sponsorable) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal members(:groucho), sponsor.sponsorable sponsor.sponsorable_type = "Firm" - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal companies(:first_firm), sponsor.sponsorable end @@ -1010,9 +1113,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.tags_count }, -1 do + assert_equal post.id, comment.id + + assert_difference "post.reload.tags_count", -1 do assert_difference "comment.reload.tags_count", +1 do tagging.taggable = comment + tagging.save! + end + end + + assert_difference "comment.reload.tags_count", -1 do + assert_difference "post.reload.tags_count", +1 do + tagging.taggable_type = post.class.polymorphic_name + tagging.taggable_id = post.id + tagging.save! end end end @@ -1122,7 +1236,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase comment.post_id = 9223372036854775808 # out of range in the bigint assert_nil comment.post - assert_not comment.valid? + assert_not_predicate comment, :valid? assert_equal [{ error: :blank }], comment.errors.details[:post] end @@ -1142,7 +1256,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase citibank.firm_id = apple.id.to_s - assert !citibank.association(:firm).stale_target? + assert_not_predicate citibank.association(:firm), :stale_target? end def test_reflect_the_most_recent_change diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index e096cd4a0b..25d55dc4c9 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -15,7 +15,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase @david = authors(:david) @thinking = posts(:thinking) @authorless = posts(:authorless) - assert @david.post_log.empty? + assert_empty @david.post_log end def test_adding_macro_callbacks @@ -96,7 +96,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_add_callback david = developers(:david) ar = projects(:active_record) - assert ar.developers_log.empty? + assert_empty ar.developers_log ar.developers_with_callbacks << david assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log ar.developers_with_callbacks << david @@ -122,12 +122,12 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert_equal alice, dev assert_not_nil new_dev assert new_dev, "record should not have been saved" - assert_not alice.new_record? + assert_not_predicate 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? + assert_empty ar.developers_log alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice assert_equal "after_adding#{alice.id}", ar.developers_log.last @@ -143,7 +143,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase david = developers(:david) jamis = developers(:jamis) activerecord = projects(:active_record) - assert activerecord.developers_log.empty? + assert_empty activerecord.developers_log activerecord.developers_with_callbacks.delete(david) assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log @@ -154,7 +154,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear activerecord = projects(:active_record) - assert activerecord.developers_log.empty? + assert_empty activerecord.developers_log if activerecord.developers_with_callbacks.size == 0 activerecord.developers << developers(:david) activerecord.developers << developers(:jamis) @@ -163,7 +163,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort assert activerecord.developers_with_callbacks.clear - assert_predicate activerecord.developers_log, :empty? + assert_empty activerecord.developers_log end def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent @@ -183,7 +183,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase @david.unchangeable_posts << @authorless rescue Exception end - assert @david.post_log.empty? + assert_empty @david.post_log assert_not_includes @david.unchangeable_posts, @authorless @david.reload assert_not_includes @david.unchangeable_posts, @authorless diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 8754889143..5fca972aee 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -8,37 +8,81 @@ module Namespaced class Post < ActiveRecord::Base self.table_name = "posts" has_one :tagging, as: :taggable, class_name: "Tagging" + + def self.polymorphic_name + sti_name + end end end -class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase +module PolymorphicFullStiClassNamesSharedTest def setup + @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) @tagging = Tagging.create(taggable: post) - @old = ActiveRecord::Base.store_full_sti_class end def teardown - ActiveRecord::Base.store_full_sti_class = @old + ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class + end + + def test_class_names + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class + post = Namespaced::Post.find_by_title("Great stuff") + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + post = Namespaced::Post.find_by_title("Great stuff") + assert_equal @tagging, post.tagging end def test_class_names_with_includes - ActiveRecord::Base.store_full_sti_class = false + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_nil post.tagging - ActiveRecord::Base.store_full_sti_class = true + ActiveRecord::Base.store_full_sti_class = store_full_sti_class post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_equal @tagging, post.tagging end def test_class_names_with_eager_load - ActiveRecord::Base.store_full_sti_class = false + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") assert_nil post.tagging - ActiveRecord::Base.store_full_sti_class = true + ActiveRecord::Base.store_full_sti_class = store_full_sti_class post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") assert_equal @tagging, post.tagging end + + def test_class_names_with_find_by + post = Namespaced::Post.find_by_title("Great stuff") + + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class + assert_nil Tagging.find_by(taggable: post) + + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + assert_equal @tagging, Tagging.find_by(taggable: post) + end +end + +class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase + include PolymorphicFullStiClassNamesSharedTest + + private + def store_full_sti_class + true + end +end + +class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase + include PolymorphicFullStiClassNamesSharedTest + + private + def store_full_sti_class + false + end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 9830917bc3..5b8d4722af 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -77,8 +77,68 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_with_scope_including_joins - assert_equal clubs(:boring_club), Member.preload(:general_club).find(1).general_club - assert_equal clubs(:boring_club), Member.eager_load(:general_club).find(1).general_club + member = Member.first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.general_club + + member = Member.preload(:general_club).first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.general_club + + member = Member.eager_load(:general_club).first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.general_club + end + + def test_loading_association_with_same_table_joins + super_memberships = [memberships(:super_membership_of_boring_club)] + + member = Member.joins(:favourite_memberships).first + assert_equal members(:groucho), member + assert_equal super_memberships, member.super_memberships + + member = Member.joins(:favourite_memberships).preload(:super_memberships).first + assert_equal members(:groucho), member + assert_equal super_memberships, member.super_memberships + + member = Member.joins(:favourite_memberships).eager_load(:super_memberships).first + assert_equal members(:groucho), member + assert_equal super_memberships, member.super_memberships + end + + def test_loading_association_with_intersection_joins + member = Member.joins(:current_membership).first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.club + assert_equal memberships(:membership_of_boring_club), member.current_membership + + member = Member.joins(:current_membership).preload(:club, :current_membership).first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.club + assert_equal memberships(:membership_of_boring_club), member.current_membership + + member = Member.joins(:current_membership).eager_load(:club, :current_membership).first + assert_equal members(:groucho), member + assert_equal clubs(:boring_club), member.club + assert_equal memberships(:membership_of_boring_club), member.current_membership + end + + def test_loading_associations_dont_leak_instance_state + assertions = ->(firm) { + assert_equal companies(:first_firm), firm + + assert_predicate firm.association(:readonly_account), :loaded? + assert_predicate firm.association(:accounts), :loaded? + + assert_equal accounts(:signals37), firm.readonly_account + assert_equal [accounts(:signals37)], firm.accounts + + assert_predicate firm.readonly_account, :readonly? + assert firm.accounts.none?(&:readonly?) + } + + assertions.call(Firm.preload(:readonly_account, :accounts).first) + assertions.call(Firm.eager_load(:readonly_account, :accounts).first) end def test_with_ordering @@ -284,7 +344,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_from_an_association_that_has_a_hash_of_conditions - assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? + assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts end def test_loading_with_no_associations @@ -1218,6 +1278,7 @@ class EagerAssociationTest < ActiveRecord::TestCase client = assert_queries(2) { Client.preload(:firm).find(c.id) } assert_no_queries { assert_nil client.firm } + assert_equal c.client_of, client.client_of end def test_preloading_empty_belongs_to_polymorphic @@ -1225,6 +1286,7 @@ class EagerAssociationTest < ActiveRecord::TestCase tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } assert_no_queries { assert_nil tagging.taggable } + assert_equal t.taggable_id, tagging.taggable_id end def test_preloading_through_empty_belongs_to @@ -1444,51 +1506,51 @@ class EagerAssociationTest < ActiveRecord::TestCase test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! - assert firm.readonly_account.readonly? + assert_predicate firm.readonly_account, :readonly? # has_and_belongs_to_many project = Project.where(id: "2").preload(:readonly_developers).first! - assert project.readonly_developers.first.readonly? + assert_predicate project.readonly_developers.first, :readonly? # has-many :through david = Author.where(id: "1").preload(:readonly_comments).first! - assert david.readonly_comments.first.readonly? + assert_predicate david.readonly_comments.first, :readonly? end test "eager-loading non-readonly association" do # has_one firm = Firm.where(id: "1").eager_load(:account).first! - assert_not firm.account.readonly? + assert_not_predicate firm.account, :readonly? # has_and_belongs_to_many project = Project.where(id: "2").eager_load(:developers).first! - assert_not project.developers.first.readonly? + assert_not_predicate project.developers.first, :readonly? # has_many :through david = Author.where(id: "1").eager_load(:comments).first! - assert_not david.comments.first.readonly? + assert_not_predicate david.comments.first, :readonly? # belongs_to post = Post.where(id: "1").eager_load(:author).first! - assert_not post.author.readonly? + assert_not_predicate post.author, :readonly? end test "eager-loading readonly association" do # has-one firm = Firm.where(id: "1").eager_load(:readonly_account).first! - assert firm.readonly_account.readonly? + assert_predicate 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? + assert_predicate project.readonly_developers.first, :readonly? # has-many :through david = Author.where(id: "1").eager_load(:readonly_comments).first! - assert david.readonly_comments.first.readonly? + assert_predicate david.readonly_comments.first, :readonly? # belongs_to post = Post.where(id: "1").eager_load(:readonly_author).first! - assert post.readonly_author.readonly? + assert_predicate post.readonly_author, :readonly? end test "preloading a polymorphic association with references to the associated table" do @@ -1501,8 +1563,10 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal posts(:welcome), post end - test "eager-loading with a polymorphic association and using the existential predicate" do - assert_equal true, authors(:david).essays.eager_load(:writer).exists? + test "eager-loading with a polymorphic association won't work consistently" do + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).to_a } + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).count } + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).exists? } end # CollectionProxy#reader is expensive, so the preloader avoids calling it. @@ -1511,6 +1575,35 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.preload(:readonly_comments).first! end + test "preloading through a polymorphic association doesn't require the association to exist" do + sponsors = [] + assert_queries 5 do + sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [:post, :membership]).to_a + end + # check the preload worked + assert_queries 0 do + sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership } + end + end + + test "preloading a regular association through a polymorphic association doesn't require the association to exist on all types" do + sponsors = [] + assert_queries 6 do + sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :first_comment }, :membership]).to_a + end + # check the preload worked + assert_queries 0 do + sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership } + end + end + + test "preloading a regular association with a typo through a polymorphic association still raises" do + # this test contains an intentional typo of first -> fist + assert_raises(ActiveRecord::AssociationNotFoundError) do + Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :fist_comment }, :membership]).to_a + end + end + private def find_all_ordered(klass, include = nil) klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a 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 c817d7267b..f414fbf64b 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 @@ -180,11 +180,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many david = Developer.find(1) - assert !david.projects.empty? + assert_not_empty david.projects assert_equal 2, david.projects.size active_record = Project.find(1) - assert !active_record.developers.empty? + assert_not_empty active_record.developers assert_equal 3, active_record.developers.size assert_includes active_record.developers, david end @@ -262,10 +262,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase no_of_projects = Project.count aredridel = Developer.new("name" => "Aredridel") aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")]) - assert !aredridel.persisted? - assert !p.persisted? + assert_not_predicate aredridel, :persisted? + assert_not_predicate p, :persisted? assert aredridel.save - assert aredridel.persisted? + assert_predicate aredridel, :persisted? assert_equal no_of_devels + 1, Developer.count assert_equal no_of_projects + 1, Project.count assert_equal 2, aredridel.projects.size @@ -311,14 +311,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert devel.projects.loaded? + assert_predicate devel.projects, :loaded? - assert !proj.persisted? + assert_not_predicate proj, :persisted? devel.save - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal devel.projects.last, proj assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -326,14 +326,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert devel.projects.loaded? + assert_predicate devel.projects, :loaded? - assert !proj.persisted? + assert_not_predicate proj, :persisted? devel.save - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal devel.projects.last, proj assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -343,10 +343,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase devel.projects.build(name: "Make bed") proj2 = devel.projects.build(name: "Lie in it") assert_equal devel.projects.last, proj2 - assert !proj2.persisted? + assert_not_predicate proj2, :persisted? devel.save - assert devel.persisted? - assert proj2.persisted? + assert_predicate devel, :persisted? + assert_predicate proj2, :persisted? assert_equal devel.projects.last, proj2 assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated end @@ -354,12 +354,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_create devel = Developer.find(1) proj = devel.projects.create("name" => "Projekt") - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -367,14 +367,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") - assert post.save - assert_equal "Yet Another Testing Title", post.title + assert post.save + assert_equal "Yet Another Testing Title", post.title # in Oracle '' is saved as null therefore need to save ' ' in not null column another_post = categories(:general).post_with_conditions.create(body: " ") - assert another_post.persisted? - assert_equal "Yet Another Testing Title", another_post.title + assert_predicate another_post, :persisted? + assert_equal "Yet Another Testing Title", another_post.title end def test_distinct_after_the_fact @@ -441,10 +441,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_removing_associations_on_destroy david = DeveloperWithBeforeDestroyRaise.find(1) - assert !david.projects.empty? + assert_not_empty david.projects david.destroy - assert david.projects.empty? - assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty? + assert_empty david.projects + assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1") end def test_destroying @@ -459,7 +459,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}") - assert join_records.empty? + assert_empty join_records assert_equal 1, david.reload.projects.size assert_equal 1, david.projects.reload.size @@ -475,7 +475,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") - assert join_records.empty? + assert_empty join_records assert_equal 0, david.reload.projects.size assert_equal 0, david.projects.reload.size @@ -484,23 +484,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_destroy_all david = Developer.find(1) david.projects.reload - assert !david.projects.empty? + assert_not_empty david.projects assert_no_difference "Project.count" do david.projects.destroy_all end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") - assert join_records.empty? + assert_empty join_records - assert david.projects.empty? - assert david.projects.reload.empty? + assert_empty david.projects + assert_empty david.projects.reload end def test_destroy_associations_destroys_multiple_associations george = parrots(:george) - assert !george.pirates.empty? - assert !george.treasures.empty? + assert_not_empty george.pirates + assert_not_empty george.treasures assert_no_difference "Pirate.count" do assert_no_difference "Treasure.count" do @@ -509,12 +509,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") - assert join_records.empty? - assert george.pirates.reload.empty? + assert_empty join_records + assert_empty george.pirates.reload join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") - assert join_records.empty? - assert george.treasures.reload.empty? + assert_empty join_records + assert_empty george.treasures.reload end def test_associations_with_conditions @@ -547,7 +547,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first assert_no_queries(ignore_none: false) do - assert project.developers.loaded? + assert_predicate project.developers, :loaded? assert_includes project.developers, developer end end @@ -557,19 +557,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first project.reload - assert ! project.developers.loaded? + assert_not_predicate project.developers, :loaded? assert_queries(1) do assert_includes project.developers, developer end - assert ! project.developers.loaded? + assert_not_predicate project.developers, :loaded? end def test_include_returns_false_for_non_matching_record_to_verify_scoping project = projects(:active_record) developer = Developer.create name: "Bryan", salary: 50_000 - assert ! project.developers.loaded? - assert ! project.developers.include?(developer) + assert_not_predicate project.developers, :loaded? + assert_not project.developers.include?(developer) end def test_find_with_merged_options @@ -662,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes developer.sym_special_projects, sp end - def test_update_attributes_after_push_without_duplicate_join_table_rows + def test_update_columns_after_push_without_duplicate_join_table_rows developer = Developer.new("name" => "Kano") project = SpecialProject.create("name" => "Special Project") assert developer.save @@ -697,24 +697,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_join_table_alias - # FIXME: `references` has no impact on the aliases generated for the join - # query. The fact that we pass `:developers_projects_join` to `references` - # and that the SQL string contains `developers_projects_join` is merely a - # coincidence. assert_equal( 3, - Developer.references(:developers_projects_join).merge( - includes: { projects: :developers }, - where: "projects_developers_projects_join.joined_on IS NOT NULL" - ).to_a.size + Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).to_a.size ) end def test_join_with_group - # FIXME: `references` has no impact on the aliases generated for the join - # query. The fact that we pass `:developers_projects_join` to `references` - # and that the SQL string contains `developers_projects_join` is merely a - # coincidence. group = Developer.columns.inject([]) do |g, c| g << "developers.#{c.name}" g << "developers_projects_2.#{c.name}" @@ -723,10 +712,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.references(:developers_projects_join).merge( - includes: { projects: :developers }, where: "projects_developers_projects_join.joined_on IS NOT NULL", - group: group.join(",") - ).to_a.size + Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).group(group.join(",")).to_a.size ) end @@ -763,9 +749,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them developer = developers(:david) - assert !developer.projects.loaded? + assert_not_predicate developer.projects, :loaded? assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort - assert !developer.projects.loaded? + assert_not_predicate developer.projects, :loaded? end def test_assign_ids diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 18548f8516..0ca902385a 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -57,7 +57,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_custom_primary_key_on_new_record_should_fetch_with_query subscriber = Subscriber.new(nick: "webster132") - assert !subscriber.subscriptions.loaded? + assert_not_predicate subscriber.subscriptions, :loaded? assert_queries 1 do assert_equal 2, subscriber.subscriptions.size @@ -68,7 +68,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_association_primary_key_on_new_record_should_fetch_with_query author = Author.new(name: "David") - assert !author.essays.loaded? + assert_not_predicate author.essays, :loaded? assert_queries 1 do assert_equal 1, author.essays.size @@ -103,7 +103,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_blank_custom_primary_key_on_new_record_should_not_run_queries author = Author.new - assert !author.essays.loaded? + assert_not_predicate author.essays, :loaded? assert_queries 0 do assert_equal 0, author.essays.size @@ -201,7 +201,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase part.reload assert_nil part.ship - assert !part.updated_at_changed? + assert_not_predicate part, :updated_at_changed? end def test_create_from_association_should_respect_default_scope @@ -444,7 +444,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_clients << company.clients_of_firm.build(name: "Another Client III") end - assert_not company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_queries(1) do assert_same new_clients[0], company.clients_of_firm.third assert_same new_clients[1], company.clients_of_firm.fourth @@ -464,7 +464,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_clients << company.clients_of_firm.build(name: "Another Client III") end - assert_not company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_queries(1) do assert_same new_clients[0], company.clients_of_firm.third! assert_same new_clients[1], company.clients_of_firm.fourth! @@ -497,8 +497,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Naruto" person.references << Reference.new - person.id = 10 - person.references person.save! assert_equal 1, person.references.update_all(favourite: true) end @@ -507,8 +505,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Sasuke" person.references << Reference.new - person.id = 10 - person.references person.save! assert_predicate person.references, :exists? end @@ -587,14 +583,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # taking from unloaded Relation bob = klass.find(authors(:bob).id) new_post = bob.posts.build - assert_not bob.posts.loaded? + assert_not_predicate bob.posts, :loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) # taking from loaded Relation bob.posts.load - assert bob.posts.loaded? + assert_predicate bob.posts, :loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) @@ -713,13 +709,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_each firm = companies(:first_firm) - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(4) do firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id } end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_each_with_conditions @@ -732,13 +728,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_in_batches firm = companies(:first_firm) - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(2) do firm.clients.find_in_batches(batch_size: 2) do |clients| @@ -746,7 +742,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_all_sanitized @@ -955,20 +951,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end def test_build company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end @@ -982,9 +978,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_collection_not_empty_after_building company = companies(:first_firm) - assert_predicate company.contracts, :empty? + assert_empty company.contracts company.contracts.build - assert_not_predicate company.contracts, :empty? + assert_not_empty company.contracts + end + + def test_collection_size_with_dirty_target + post = posts(:thinking) + assert_equal [], post.reader_ids + assert_equal 0, post.readers.size + post.readers.reset + post.readers.build + assert_equal [nil], post.reader_ids + assert_equal 1, post.readers.size + end + + def test_collection_empty_with_dirty_target + post = posts(:thinking) + assert_equal [], post.reader_ids + assert_empty post.readers + post.readers.reset + post.readers.build + assert_equal [nil], post.reader_ids + assert_not_empty post.readers end def test_collection_size_twice_for_regressions @@ -1008,7 +1024,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_followed_by_save_does_not_load_target companies(:first_firm).clients_of_firm.build("name" => "Another Client") assert companies(:first_firm).save - assert !companies(:first_firm).clients_of_firm.loaded? + assert_not_predicate companies(:first_firm).clients_of_firm, :loaded? end def test_build_without_loading_association @@ -1028,10 +1044,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end @@ -1069,7 +1085,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_predicate companies(:first_firm).clients_of_firm, :loaded? new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last end @@ -1082,7 +1098,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_followed_by_save_does_not_load_target companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert companies(:first_firm).save - assert !companies(:first_firm).clients_of_firm.loaded? + assert_not_predicate companies(:first_firm).clients_of_firm, :loaded? end def test_deleting @@ -1108,7 +1124,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # option is not given on the association. ship = Ship.create(name: "Countless", treasures_count: 10) - assert_not Ship.reflect_on_association(:treasures).has_cached_counter? + assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter? # Count should come from sql count() of treasures rather than treasures_count attribute assert_equal ship.treasures.size, 0 @@ -1199,7 +1215,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_empty_with_counter_cache post = posts(:welcome) assert_queries(0) do - assert_not post.comments.empty? + assert_not_empty post.comments end end @@ -1211,20 +1227,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_update_attributes_on_id_changes_the_counter_cache + def test_calling_update_on_id_changes_the_counter_cache topic = Topic.order("id ASC").first original_count = topic.replies.to_a.size assert_equal original_count, topic.replies_count first_reply = topic.replies.first - first_reply.update_attributes(parent_id: nil) + first_reply.update(parent_id: nil) assert_equal original_count - 1, topic.reload.replies_count - first_reply.update_attributes(parent_id: topic.id) + first_reply.update(parent_id: topic.id) assert_equal original_count, topic.reload.replies_count end - def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + def test_calling_update_changing_ids_doesnt_change_counter_cache topic1 = Topic.find(1) topic2 = Topic.find(3) original_count1 = topic1.replies.to_a.size @@ -1233,11 +1249,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase reply1 = topic1.replies.first reply2 = topic2.replies.first - reply1.update_attributes(parent_id: topic2.id) + reply1.update(parent_id: topic2.id) assert_equal original_count1 - 1, topic1.reload.replies_count assert_equal original_count2 + 1, topic2.reload.replies_count - reply2.update_attributes(parent_id: topic1.id) + reply2.update(parent_id: topic1.id) assert_equal original_count1, topic1.reload.replies_count assert_equal original_count2, topic2.reload.replies_count end @@ -1441,13 +1457,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_creation_respects_hash_condition ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build - assert ms_client.save - assert_equal "Microsoft", ms_client.name + assert ms_client.save + assert_equal "Microsoft", ms_client.name another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create - assert another_ms_client.persisted? - assert_equal "Microsoft", another_ms_client.name + assert_predicate another_ms_client, :persisted? + assert_equal "Microsoft", another_ms_client.name end def test_clearing_without_initial_access @@ -1558,7 +1574,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_predicate companies(:first_firm).clients_of_firm, :loaded? clients = companies(:first_firm).clients_of_firm.to_a - assert !clients.empty?, "37signals has clients after load" + assert_not clients.empty?, "37signals has clients after load" destroyed = companies(:first_firm).clients_of_firm.destroy_all assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" @@ -1570,7 +1586,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 3, firm.clients.size firm.destroy - assert Client.all.merge!(where: "firm_id=#{firm.id}").to_a.empty? + assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a end def test_dependence_for_associations_with_hash_condition @@ -1633,7 +1649,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithExceptionFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedWithExceptionFirm.exists?(name: "restrict") assert firm.companies.exists?(name: "child") @@ -1643,11 +1659,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") @@ -1660,11 +1676,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") @@ -1716,8 +1732,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase account = Account.new orig_accounts = firm.accounts.to_a - assert !account.valid? - assert !orig_accounts.empty? + assert_not_predicate account, :valid? + assert_not_empty orig_accounts error = assert_raise ActiveRecord::RecordNotSaved do firm.accounts = [account] end @@ -1775,9 +1791,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them company = companies(:first_firm) - assert !company.clients.loaded? + assert_not_predicate company.clients, :loaded? assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids - assert !company.clients.loaded? + assert_not_predicate company.clients, :loaded? end def test_counter_cache_on_unloaded_association @@ -1842,6 +1858,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } end + def test_associations_order_should_be_priority_over_throughs_order + david = authors(:david) + expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1] + assert_equal expected, david.comments_desc.map(&:id) + assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id) + end + def test_dynamic_find_should_respect_association_order_for_through assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment") @@ -1859,7 +1882,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client = firm.clients.first assert_no_queries do - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? assert_equal true, firm.clients.include?(client) end end @@ -1869,18 +1892,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client = firm.clients.first firm.reload - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(1) do assert_equal true, firm.clients.include?(client) end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) client = Client.create!(name: "Not Associated") - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_equal false, firm.clients.include?(client) end @@ -1889,13 +1912,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.first firm.clients.second firm.clients.last - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) firm.clients.load_target - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? assert_no_queries(ignore_none: false) do firm.clients.first @@ -1908,7 +1931,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_existing_record_with_build_should_load_association firm = companies(:first_firm) firm.clients.build(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 1 do firm.clients.first @@ -1916,13 +1939,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.last end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association firm = companies(:first_firm) firm.clients.create(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 3 do firm.clients.first @@ -1930,7 +1953,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.last end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_first_nth_or_last_on_new_record_should_not_run_queries @@ -1946,14 +1969,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 2 do firm.clients.first(2) firm.clients.last(2) end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_many_should_count_instead_of_loading_association @@ -1961,7 +1984,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.many? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_many_on_loaded_association_should_not_use_query @@ -1973,25 +1996,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_many_should_defer_to_collection_if_using_a_block firm = companies(:first_firm) assert_queries(1) do - firm.clients.expects(:size).never - firm.clients.many? { true } + assert_not_called(firm.clients, :size) do + firm.clients.many? { true } + end end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_many_should_return_false_if_none_or_one firm = companies(:another_firm) - assert !firm.clients_like_ms.many? + assert_not_predicate firm.clients_like_ms, :many? assert_equal 0, firm.clients_like_ms.size firm = companies(:first_firm) - assert !firm.limited_clients.many? + assert_not_predicate firm.limited_clients, :many? assert_equal 1, firm.limited_clients.size end def test_calling_many_should_return_true_if_more_than_one firm = companies(:first_firm) - assert firm.clients.many? + assert_predicate firm.clients, :many? assert_equal 3, firm.clients.size end @@ -2000,33 +2024,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.none? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_none_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.none? } + assert_no_queries { assert_not firm.clients.none? } end def test_calling_none_should_defer_to_collection_if_using_a_block firm = companies(:first_firm) assert_queries(1) do - firm.clients.expects(:size).never - firm.clients.none? { true } + assert_not_called(firm.clients, :size) do + firm.clients.none? { true } + end end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_none_should_return_true_if_none firm = companies(:another_firm) - assert firm.clients_like_ms.none? + assert_predicate firm.clients_like_ms, :none? assert_equal 0, firm.clients_like_ms.size end def test_calling_none_should_return_false_if_any firm = companies(:first_firm) - assert !firm.limited_clients.none? + assert_not_predicate firm.limited_clients, :none? assert_equal 1, firm.limited_clients.size end @@ -2035,39 +2060,40 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.one? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_one_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.one? } + assert_no_queries { assert_not firm.clients.one? } end def test_calling_one_should_defer_to_collection_if_using_a_block firm = companies(:first_firm) assert_queries(1) do - firm.clients.expects(:size).never - firm.clients.one? { true } + assert_not_called(firm.clients, :size) do + firm.clients.one? { true } + end end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_one_should_return_false_if_zero firm = companies(:another_firm) - assert ! firm.clients_like_ms.one? + assert_not_predicate firm.clients_like_ms, :one? assert_equal 0, firm.clients_like_ms.size end def test_calling_one_should_return_true_if_one firm = companies(:first_firm) - assert firm.limited_clients.one? + assert_predicate firm.limited_clients, :one? assert_equal 1, firm.limited_clients.size end def test_calling_one_should_return_false_if_more_than_one firm = companies(:first_firm) - assert ! firm.clients.one? + assert_not_predicate firm.clients, :one? assert_equal 3, firm.clients.size end @@ -2089,9 +2115,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Comment.expects(:transaction) - Post.first.comments.transaction do - # nothing + assert_called(Comment, :transaction) do + Post.first.comments.transaction do + # nothing + end end end @@ -2287,7 +2314,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) assert post.taggings_with_delete_all.count > 0 - assert !post.taggings_with_delete_all.loaded? + assert_not_predicate post.taggings_with_delete_all, :loaded? # 2 queries: one DELETE and another to update the counter cache assert_queries(2) do @@ -2309,7 +2336,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "collection proxy respects default scope" do author = authors(:mary) - assert !author.first_posts.exists? + assert_not_predicate author.first_posts, :exists? end test "association with extend option" do @@ -2428,7 +2455,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase pirate = FamousPirate.new pirate.famous_ships << ship = FamousShip.new - assert pirate.valid? + assert_predicate pirate, :valid? assert_not pirate.valid?(:conference) assert_equal "can't be blank", ship.errors[:name].first end @@ -2560,6 +2587,70 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + test "calling size on an association that has not been loaded performs a query" do + car = Car.create! + Bulb.create(car_id: car.id) + + car_two = Car.create! + + assert_queries(1) do + assert_equal 1, car.bulbs.size + end + + assert_queries(1) do + assert_equal 0, car_two.bulbs.size + end + end + + test "calling size on an association that has been loaded does not perform query" do + car = Car.create! + Bulb.create(car_id: car.id) + car.bulb_ids + + car_two = Car.create! + car_two.bulb_ids + + assert_no_queries do + assert_equal 1, car.bulbs.size + end + + assert_no_queries do + assert_equal 0, car_two.bulbs.size + end + end + + test "calling empty on an association that has not been loaded performs a query" do + car = Car.create! + Bulb.create(car_id: car.id) + + car_two = Car.create! + + assert_queries(1) do + assert_not_empty car.bulbs + end + + assert_queries(1) do + assert_empty car_two.bulbs + end + end + + test "calling empty on an association that has been loaded does not performs query" do + car = Car.create! + Bulb.create(car_id: car.id) + car.bulb_ids + + car_two = Car.create! + car_two.bulb_ids + + assert_no_queries do + assert_not_empty car.bulbs + end + + assert_no_queries do + assert_empty car_two.bulbs + end + end + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base self.table_name = "authors" has_many :posts_with_error_destroying, @@ -2614,6 +2705,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_create_children_could_be_rolled_back_by_after_save + firm = Firm.create!(name: "A New Firm, Inc") + assert_no_difference "Client.count" do + client = firm.clients.create(name: "New Client") do |cli| + cli.rollback_on_save = true + assert_not cli.rollback_on_create_called + end + assert client.rollback_on_create_called + end + end + private def force_signal37_to_load_all_clients_of_firm diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 7c9c9e81ab..d5573b6d02 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create person_id: 0, post_id: 0 end + def test_marshal_dump + preloaded = Post.includes(:first_blue_tags).first + assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) + end + def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do @@ -353,10 +358,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(1) do - assert posts(:welcome).people.empty? + assert_empty posts(:welcome).people end - assert posts(:welcome).reload.people.reload.empty? + assert_empty posts(:welcome).reload.people.reload end def test_destroy_association @@ -366,8 +371,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people.reload.empty? + assert_empty posts(:welcome).reload.people + assert_empty posts(:welcome).people.reload end def test_destroy_all @@ -377,8 +382,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people.reload.empty? + assert_empty posts(:welcome).reload.people + assert_empty posts(:welcome).people.reload end def test_should_raise_exception_for_destroying_mismatching_records @@ -538,6 +543,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_update_counter_caches_on_destroy_with_indestructible_through_record + post = posts(:welcome) + tag = post.indestructible_tags.create!(name: "doomed") + post.update_columns(indestructible_tags_count: post.indestructible_tags.count) + + assert_no_difference "post.reload.indestructible_tags_count" do + posts(:welcome).indestructible_tags.destroy(tag) + end + end + def test_replace_association assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload } @@ -675,10 +690,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(0) do - assert posts(:welcome).people.empty? + assert_empty posts(:welcome).people end - assert posts(:welcome).reload.people.reload.empty? + assert_empty posts(:welcome).reload.people.reload end def test_association_callback_ordering @@ -722,6 +737,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :before, "Roger"], [:added, :after, "Roger"] ], log.last(4) + + post.people_with_callbacks.build { |person| person.first_name = "Ted" } + assert_equal [ + [:added, :before, "Ted"], + [:added, :after, "Ted"] + ], log.last(2) + + post.people_with_callbacks.create { |person| person.first_name = "Sam" } + assert_equal [ + [:added, :before, "Sam"], + [:added, :after, "Sam"] + ], log.last(2) end def test_dynamic_find_should_respect_association_include @@ -760,9 +787,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them person = people(:michael) - assert !person.posts.loaded? + assert_not_predicate person.posts, :loaded? assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort - assert !person.posts.loaded? + assert_not_predicate person.posts, :loaded? end def test_association_proxy_transaction_method_starts_transaction_in_association_class @@ -851,8 +878,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:mary) category = author.named_categories.create(name: "Primary") author.named_categories.delete(category) - assert !Categorization.exists?(author_id: author.id, named_category_name: category.name) - assert author.named_categories.reload.empty? + assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name) + assert_empty author.named_categories.reload end def test_collection_singular_ids_getter_with_string_primary_keys @@ -934,8 +961,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_through_association_readonly_should_be_false - assert !people(:michael).posts.first.readonly? - assert !people(:michael).posts.to_a.first.readonly? + assert_not_predicate people(:michael).posts.first, :readonly? + assert_not_predicate people(:michael).posts.to_a.first, :readonly? end def test_can_update_through_association @@ -1024,12 +1051,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post.author_categorizations proxy = post.send(:association_instance_get, :author_categorizations) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) post.author_id = authors(:david).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) end @@ -1046,7 +1073,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_includes post.author_addresses, address post.author_addresses.delete(address) - assert post[:author_count].nil? + assert_predicate post[:author_count], :nil? end def test_primary_key_option_on_source @@ -1262,6 +1289,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal authors(:david), Author.joins(:comments_for_first_author).take end + def test_has_many_through_with_left_joined_same_table_with_through_table + assert_equal [comments(:eager_other_comment1)], authors(:mary).comments.left_joins(:post) + end + def test_has_many_through_with_unscope_should_affect_to_through_scope assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments end @@ -1308,6 +1339,70 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_many_through_update_ids_with_conditions + author = Author.create!(name: "Bill") + category = categories(:general) + + author.update( + special_categories_with_condition_ids: [category.id], + nonspecial_categories_with_condition_ids: [category.id] + ) + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [category.id], author.nonspecial_categories_with_condition_ids + + author.update(nonspecial_categories_with_condition_ids: []) + author.reload + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [], author.nonspecial_categories_with_condition_ids + end + + def test_single_has_many_through_association_with_unpersisted_parent_instance + post_with_single_has_many_through = Class.new(Post) do + def self.name; "PostWithSingleHasManyThrough"; end + has_many :subscriptions, through: :author + end + post = post_with_single_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on single has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on single has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + + def test_nested_has_many_through_association_with_unpersisted_parent_instance + post_with_nested_has_many_through = Class.new(Post) do + def self.name; "PostWithNestedHasManyThrough"; end + has_many :books, through: :author + has_many :subscriptions, through: :books + end + post = post_with_nested_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on nested has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on nested has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + private def make_model(name) Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index ec5d95080b..d7e898a1c0 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -114,8 +114,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase developer = Developer.create!(name: "Someone") ship = Ship.create!(name: "Planet Caravan", developer: developer) ship.destroy - assert !ship.persisted? - assert !developer.persisted? + assert_not_predicate ship, :persisted? + assert_not_predicate developer, :persisted? end def test_natural_assignment_to_nil_after_destroy @@ -186,7 +186,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedWithExceptionFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? end def test_restrict_with_error @@ -197,10 +197,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? end def test_restrict_with_error_with_locale @@ -213,10 +213,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? ensure I18n.backend.reload! end @@ -377,7 +377,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) - assert a.persisted? + assert_predicate a, :persisted? assert_equal a, firm.account assert_equal a, firm.account firm.association(:account).reload @@ -395,7 +395,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_cant_save_readonly_association assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! } - assert companies(:first_firm).readonly_account.readonly? + assert_predicate companies(:first_firm).readonly_account, :readonly? end def test_has_one_proxy_should_not_respond_to_private_methods @@ -433,7 +433,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_create_respects_hash_condition account = companies(:first_firm).create_account_limit_500_with_hash_conditions - assert account.persisted? + assert_predicate account, :persisted? assert_equal 500, account.credit_limit end @@ -450,9 +450,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase new_ship = pirate.create_ship assert_not_equal ships(:black_pearl), new_ship assert_equal new_ship, pirate.ship - assert new_ship.new_record? + assert_predicate new_ship, :new_record? assert_nil orig_ship.pirate_id - assert !orig_ship.changed? # check it was saved + assert_not orig_ship.changed? # check it was saved end def test_creation_failure_with_dependent_option @@ -460,8 +460,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase orig_ship = pirate.dependent_ship new_ship = pirate.create_dependent_ship - assert new_ship.new_record? - assert orig_ship.destroyed? + assert_predicate new_ship, :new_record? + assert_predicate orig_ship, :destroyed? end def test_creation_failure_due_to_new_record_should_raise_error @@ -481,7 +481,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate = pirates(:blackbeard) pirate.ship.name = nil - assert !pirate.ship.valid? + assert_not_predicate pirate.ship, :valid? error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = ships(:interceptor) end @@ -588,7 +588,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase ship.save! ship.name = "new name" - assert ship.changed? + assert_predicate ship, :changed? assert_queries(1) do # One query for updating name, not triggering query for updating pirate_id pirate.ship = ship @@ -678,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end def test_association_enum_works_properly_with_nested_join @@ -725,4 +725,28 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_not DestroyByParentBook.exists?(book.id) end + + class UndestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyableAuthor" + before_destroy :dont + + def dont + throw(:abort) + end + end + + class DestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy + end + + def test_dependency_should_halt_parent_destruction + author = DestroyableAuthor.create!(name: "Test") + UndestroyableBook.create!(author: author) + + assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do + assert_not author.destroy + end + 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 1d37457464..0309663943 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -42,6 +42,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil new_member.club end + def test_creating_association_builds_through_record + new_member = Member.create(name: "Chris") + new_club = new_member.association(:club).build + assert new_member.current_membership + assert_equal new_club, new_member.club + assert_predicate new_club, :new_record? + assert_predicate new_member.current_membership, :new_record? + assert new_member.save + assert_predicate new_club, :persisted? + assert_predicate new_member.current_membership, :persisted? + end + def test_creating_association_builds_through_record_for_new new_member = Member.new(name: "Jane") new_member.club = clubs(:moustache_club) @@ -52,6 +64,24 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_building_multiple_associations_builds_through_record + member_type = MemberType.create! + member = Member.create! + member_detail_with_one_association = MemberDetail.new(member_type: member_type) + assert_predicate member_detail_with_one_association.member, :new_record? + member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member) + assert_predicate member_detail_with_two_associations.member, :new_record? + end + + def test_creating_multiple_associations_creates_through_record + member_type = MemberType.create! + member = Member.create! + member_detail_with_one_association = MemberDetail.create!(member_type: member_type) + assert_not_predicate member_detail_with_one_association.member, :new_record? + member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member) + assert_not_predicate member_detail_with_two_associations.member, :new_record? + end + def test_creating_association_sets_both_parent_ids_for_new member = Member.new(name: "Sean Griffin") club = Club.new(name: "Da Club") @@ -229,7 +259,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase MemberDetail.all.merge!(includes: :member_type).to_a end @new_detail = @member_details[0] - assert @new_detail.send(:association, :member_type).loaded? + assert_predicate @new_detail.send(:association, :member_type), :loaded? assert_no_queries { @new_detail.member_type } end @@ -317,12 +347,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.dashboard proxy = minivan.send(:association_instance_get, :dashboard) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal dashboards(:cool_first), minivan.dashboard minivan.speedometer_id = speedometers(:second).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal dashboards(:second), minivan.dashboard end @@ -334,7 +364,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.speedometer_id = speedometers(:second).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal dashboards(:second), minivan.dashboard end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 7be875fec6..c33dcdee61 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -79,19 +79,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_implicit_inner_joins_honors_readonly_with_select authors = Author.joins(:posts).select("authors.*").to_a - assert !authors.empty?, "expected authors to be non-empty" + assert_not authors.empty?, "expected authors to be non-empty" assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_honors_readonly_false authors = Author.joins(:posts).readonly(false).to_a - assert !authors.empty?, "expected authors to be non-empty" + assert_not authors.empty?, "expected authors to be non-empty" assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_does_not_set_associations authors = Author.joins(:posts).select("authors.*").to_a - assert !authors.empty?, "expected authors to be non-empty" + assert_not authors.empty?, "expected authors to be non-empty" assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded" end @@ -115,19 +115,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase scope = Post.joins(:special_comments).where(id: posts(:sti_comments).id) # The join should match SpecialComment and its subclasses only - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_find_with_conditions_on_reflection - assert !posts(:welcome).comments.empty? + assert_not_empty posts(:welcome).comments assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!] end def test_find_with_conditions_on_through_reflection - assert !posts(:welcome).tags.empty? - assert Post.joins(:misc_tags).where(id: posts(:welcome).id).empty? + assert_not_empty posts(:welcome).tags + assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id) end test "the default scope of the target is applied when joining associations" do diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index c0d328ca8a..da3a42e2b5 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -119,17 +119,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses sponsor_reflection = Sponsor.reflect_on_association(:sponsorable) - assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically" + assert_not sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically" club_reflection = Club.reflect_on_association(:members) - assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" + assert_not club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end def test_polymorphic_has_one_should_find_inverse_automatically man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) - assert man_reflection.has_inverse? + assert_predicate man_reflection, :has_inverse? end end @@ -150,22 +150,22 @@ class InverseAssociationTests < ActiveRecord::TestCase def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse has_one_with_inverse_ref = Man.reflect_on_association(:face) - assert has_one_with_inverse_ref.has_inverse? + assert_predicate has_one_with_inverse_ref, :has_inverse? has_many_with_inverse_ref = Man.reflect_on_association(:interests) - assert has_many_with_inverse_ref.has_inverse? + assert_predicate has_many_with_inverse_ref, :has_inverse? belongs_to_with_inverse_ref = Face.reflect_on_association(:man) - assert belongs_to_with_inverse_ref.has_inverse? + assert_predicate belongs_to_with_inverse_ref, :has_inverse? has_one_without_inverse_ref = Club.reflect_on_association(:sponsor) - assert !has_one_without_inverse_ref.has_inverse? + assert_not_predicate has_one_without_inverse_ref, :has_inverse? has_many_without_inverse_ref = Club.reflect_on_association(:memberships) - assert !has_many_without_inverse_ref.has_inverse? + assert_not_predicate has_many_without_inverse_ref, :has_inverse? belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club) - assert !belongs_to_without_inverse_ref.has_inverse? + assert_not_predicate belongs_to_without_inverse_ref, :has_inverse? end def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of @@ -190,6 +190,16 @@ class InverseAssociationTests < ActiveRecord::TestCase assert_nil belongs_to_ref.inverse_of end + def test_polymorphic_associations_dont_attempt_to_find_inverse_of + belongs_to_ref = Sponsor.reflect_on_association(:sponsor) + assert_raise(ArgumentError) { belongs_to_ref.klass } + assert_nil belongs_to_ref.inverse_of + + belongs_to_ref = Face.reflect_on_association(:human) + assert_raise(ArgumentError) { belongs_to_ref.klass } + assert_nil belongs_to_ref.inverse_of + end + def test_this_inverse_stuff firm = Firm.create!(name: "Adequate Holdings") Project.create!(name: "Project 1", firm: firm) @@ -464,7 +474,7 @@ class InverseHasManyTests < ActiveRecord::TestCase interest = Interest.create!(man: man) man.interests.find(interest.id) - assert_not man.interests.loaded? + assert_not_predicate man.interests, :loaded? end def test_raise_record_not_found_error_when_invalid_ids_are_passed @@ -504,16 +514,16 @@ class InverseHasManyTests < ActiveRecord::TestCase i.man.name = "Charles" assert_equal i.man.name, man.name - assert !man.persisted? + assert_not_predicate man, :persisted? end def test_inverse_instance_should_be_set_before_find_callbacks_are_run reset_callbacks(Interest, :find) do Interest.after_find { raise unless association(:man).loaded? && man.present? } - assert Man.first.interests.reload.any? - assert Man.includes(:interests).first.interests.any? - assert Man.joins(:interests).includes(:interests).first.interests.any? + assert_predicate Man.first.interests.reload, :any? + assert_predicate Man.includes(:interests).first.interests, :any? + assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? end end @@ -521,9 +531,9 @@ class InverseHasManyTests < ActiveRecord::TestCase reset_callbacks(Interest, :initialize) do Interest.after_initialize { raise unless association(:man).loaded? && man.present? } - assert Man.first.interests.reload.any? - assert Man.includes(:interests).first.interests.any? - assert Man.joins(:interests).includes(:interests).first.interests.any? + assert_predicate Man.first.interests.reload, :any? + assert_predicate Man.includes(:interests).first.interests, :any? + assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 5d83c9435b..9d1c73c33b 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -44,11 +44,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_distinct_through_count author = authors(:mary) - assert !authors(:mary).unique_categorized_posts.loaded? + assert_not_predicate authors(:mary).unique_categorized_posts, :loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) } assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) } - assert !authors(:mary).unique_categorized_posts.loaded? + assert_not_predicate authors(:mary).unique_categorized_posts, :loaded? end def test_has_many_distinct_through_find @@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase Tag.has_many :null_taggings, -> { none }, class_name: :Tagging Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post" assert_equal [], tags(:general).null_tagged_posts - refute_equal [], tags(:general).tagged_posts + assert_not_equal [], tags(:general).tagged_posts end def test_eager_has_many_polymorphic_with_source_type @@ -454,8 +454,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_uses_conditions_specified_on_the_has_many_association author = Author.first - assert author.comments.present? - assert author.nonexistent_comments.blank? + assert_predicate author.comments, :present? + assert_predicate author.nonexistent_comments, :blank? end def test_has_many_through_uses_correct_attributes @@ -468,26 +468,26 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase saved_post.tags << new_tag assert new_tag.persisted? # consistent with habtm! - assert saved_post.persisted? + assert_predicate saved_post, :persisted? assert_includes saved_post.tags, new_tag - assert new_tag.persisted? + assert_predicate new_tag, :persisted? assert_includes saved_post.reload.tags.reload, new_tag new_post = Post.new(title: "Association replacement works!", body: "You best believe it.") saved_tag = tags(:general) new_post.tags << saved_tag - assert !new_post.persisted? - assert saved_tag.persisted? + assert_not_predicate new_post, :persisted? + assert_predicate saved_tag, :persisted? assert_includes new_post.tags, saved_tag new_post.save! - assert new_post.persisted? + assert_predicate new_post, :persisted? assert_includes new_post.reload.tags.reload, saved_tag - assert !posts(:thinking).tags.build.persisted? - assert !posts(:thinking).tags.new.persisted? + assert_not_predicate posts(:thinking).tags.build, :persisted? + assert_not_predicate posts(:thinking).tags.new, :persisted? end def test_create_associate_when_adding_to_has_many_through @@ -529,14 +529,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded author = authors(:david) assert_equal 10, author.comments.size - assert !author.comments.loaded? + assert_not_predicate author.comments, :loaded? end def test_has_many_through_collection_size_uses_counter_cache_if_it_exists c = categories(:general) c.categorizations_count = 100 assert_equal 100, c.categorizations.size - assert !c.categorizations.loaded? + assert_not_predicate c.categorizations, :loaded? end def test_adding_junk_to_has_many_through_should_raise_type_mismatch @@ -710,7 +710,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = david.categories.first assert_no_queries do - assert david.categories.loaded? + assert_predicate david.categories, :loaded? assert_includes david.categories, category end end @@ -720,19 +720,19 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = david.categories.first david.reload - assert ! david.categories.loaded? + assert_not_predicate david.categories, :loaded? assert_queries(1) do assert_includes david.categories, category end - assert ! david.categories.loaded? + assert_not_predicate david.categories, :loaded? end def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping david = authors(:david) category = Category.create!(name: "Not Associated") - assert ! david.categories.loaded? - assert ! david.categories.include?(category) + assert_not_predicate david.categories, :loaded? + assert_not david.categories.include?(category) end def test_has_many_through_goes_through_all_sti_classes diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index c95d0425cd..0e54e8c1b0 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,6 +5,7 @@ require "models/post" require "models/comment" require "models/author" require "models/essay" +require "models/category" require "models/categorization" require "models/person" @@ -69,15 +70,15 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id) # The join should match SpecialComment and its subclasses only - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_does_not_override_select authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts) - assert authors.any? - assert authors.first.respond_to?(:addr_id) + assert_predicate authors, :any? + assert_respond_to authors.first, :addr_id end test "the default scope of the target is applied when joining associations" do diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 65d30d011b..03ed1c1d47 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -78,7 +78,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # This ensures that the polymorphism of taggings is being observed correctly authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors end # has_many through @@ -177,7 +177,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase members = Member.joins(:organization_member_details). where("member_details.id" => 9) - assert members.empty? + assert_empty members end # has_many through @@ -209,7 +209,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase members = Member.joins(:organization_member_details_2). where("member_details.id" => 9) - assert members.empty? + assert_empty members end # has_many through @@ -425,9 +425,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where("taggings.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors end def test_nested_has_many_through_with_scope_on_polymorphic_reflection @@ -456,9 +456,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Ensure STI is respected in the join scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id) - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_has_many_through_with_sti_on_nested_through_reflection @@ -466,8 +466,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal [taggings(:special_comment_rating)], taggings scope = Post.joins(:special_comments_ratings_taggings).where(id: posts(:sti_comments).id) - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") end def test_nested_has_many_through_writers_should_raise_error @@ -517,7 +517,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_nested_has_many_through_with_conditions_on_through_associations_preload - assert Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags).empty? + assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags) authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) } blue = tags(:blue) @@ -574,9 +574,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase c = Categorization.new c.author = authors(:david) c.post_taggings.to_a - assert !c.post_taggings.empty? + assert_not_empty c.post_taggings c.save - assert !c.post_taggings.empty? + assert_not_empty c.post_taggings end def test_polymorphic_has_many_through_when_through_association_has_not_loaded diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index de04221ea6..739eb02e0c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -47,7 +47,7 @@ class AssociationsTest < ActiveRecord::TestCase ship = Ship.create!(name: "The good ship Dollypop") part = ship.parts.create!(name: "Mast") part.mark_for_destruction - assert ship.parts[0].marked_for_destruction? + assert_predicate ship.parts[0], :marked_for_destruction? end def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction @@ -92,7 +92,7 @@ class AssociationsTest < ActiveRecord::TestCase firm.clients.reload - assert !firm.clients.empty?, "New firm should have reloaded client objects" + assert_not firm.clients.empty?, "New firm should have reloaded client objects" assert_equal 1, firm.clients.size, "New firm should have reloaded clients count" end @@ -102,8 +102,8 @@ class AssociationsTest < ActiveRecord::TestCase has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)] mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable" - assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable" - assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass" + assert_not using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable" + assert_not using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass" end def test_association_with_references @@ -119,7 +119,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!")) - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? assert_includes david.posts, post end @@ -127,7 +127,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.categories << categories(:technology) - assert !david.categories.loaded? + assert_not_predicate david.categories, :loaded? assert_includes david.categories, categories(:technology) end @@ -135,23 +135,23 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!")) - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? david.save - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? assert_includes david.posts, post end def test_push_does_not_lose_additions_to_new_record josh = Author.new(name: "Josh") josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!") - assert josh.posts.loaded? + assert_predicate josh.posts, :loaded? assert_equal 1, josh.posts.size end def test_append_behaves_like_push josh = Author.new(name: "Josh") josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!") - assert josh.posts.loaded? + assert_predicate josh.posts, :loaded? assert_equal 1, josh.posts.size end @@ -163,22 +163,22 @@ class AssociationProxyTest < ActiveRecord::TestCase def test_save_on_parent_does_not_load_target david = developers(:david) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? david.update_columns(created_at: Time.now) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? end def test_load_does_load_target david = developers(:david) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? david.projects.load - assert david.projects.loaded? + assert_predicate david.projects, :loaded? end def test_inspect_does_not_reload_a_not_yet_loaded_target andreas = Developer.new name: "Andreas", log: "new developer added" - assert !andreas.audit_logs.loaded? + assert_not_predicate andreas.audit_logs, :loaded? assert_match(/message: "new developer added"/, andreas.audit_logs.inspect) end @@ -248,14 +248,14 @@ class AssociationProxyTest < ActiveRecord::TestCase test "first! works on loaded associations" do david = authors(:david) assert_equal david.first_posts.first, david.first_posts.reload.first! - assert david.first_posts.loaded? + assert_predicate david.first_posts, :loaded? assert_no_queries { david.first_posts.first! } end def test_pluck_uses_loaded_target david = authors(:david) assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title) - assert david.first_posts.loaded? + assert_predicate david.first_posts, :loaded? assert_no_queries { david.first_posts.pluck(:title) } end @@ -263,9 +263,9 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts.reload - assert david.posts.loaded? + assert_predicate david.posts, :loaded? david.posts.reset - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? end end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 0170a6e98d..54512068ee 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,7 +12,7 @@ module ActiveRecord def setup @klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do def self.superclass; Base; end - def self.base_class; self; end + def self.base_class?; true; end def self.decorate_matching_attribute_types(*); end include ActiveRecord::DefineCallbacks diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index c48f7d3518..0bfd46a522 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -63,8 +63,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") - assert !t.attribute_present?("content") - assert !t.attribute_present?("author_name") + assert_not t.attribute_present?("content") + assert_not t.attribute_present?("author_name") end test "attribute_present with booleans" do @@ -77,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert b2.attribute_present?(:value) b3 = Boolean.new - assert !b3.attribute_present?(:value) + assert_not b3.attribute_present?(:value) b4 = Boolean.new b4.value = false @@ -99,8 +99,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase end test "boolean attributes" do - assert !Topic.find(1).approved? - assert Topic.find(2).approved? + assert_not_predicate Topic.find(1), :approved? + assert_predicate Topic.find(2), :approved? end test "set attributes" do @@ -142,16 +142,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_respond_to topic, :title= assert_respond_to topic, "author_name" assert_respond_to topic, "attribute_names" - assert !topic.respond_to?("nothingness") - assert !topic.respond_to?(:nothingness) + assert_not_respond_to topic, "nothingness" + assert_not_respond_to topic, :nothingness end test "respond_to? with a custom primary key" do keyboard = Keyboard.create assert_not_nil keyboard.key_number assert_equal keyboard.key_number, keyboard.id - assert keyboard.respond_to?("key_number") - assert keyboard.respond_to?("id") + assert_respond_to keyboard, "key_number" + assert_respond_to keyboard, "id" end test "id_before_type_cast with a custom primary key" do @@ -163,19 +163,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number) end - # Syck calls respond_to? before actually calling initialize. - test "respond_to? with an allocated object" do - 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" - assert_respond_to topic, :title - end - # IRB inspects the return value of MyModel.allocate. test "allocated objects can be inspected" do topic = Topic.allocate @@ -354,9 +341,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "read_attribute when false" do topic = topics(:first) topic.approved = false - assert !topic.approved?, "approved should be false" + assert_not topic.approved?, "approved should be false" topic.approved = "false" - assert !topic.approved?, "approved should be false" + assert_not topic.approved?, "approved should be false" end test "read_attribute when true" do @@ -370,10 +357,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "boolean attributes writing and reading" do topic = Topic.new topic.approved = "false" - assert !topic.approved?, "approved should be false" + assert_not topic.approved?, "approved should be false" topic.approved = "false" - assert !topic.approved?, "approved should be false" + assert_not topic.approved?, "approved should be false" topic.approved = "true" assert topic.approved?, "approved should be true" @@ -457,30 +444,30 @@ class AttributeMethodsTest < ActiveRecord::TestCase SQL assert_equal "Firm", object.string_value - assert object.string_value? + assert_predicate object, :string_value? object.string_value = " " - assert !object.string_value? + assert_not_predicate object, :string_value? assert_equal 1, object.int_value.to_i - assert object.int_value? + assert_predicate object, :int_value? object.int_value = "0" - assert !object.int_value? + assert_not_predicate object, :int_value? end test "non-attribute read and write" do topic = Topic.new - assert !topic.respond_to?("mumbo") + assert_not_respond_to topic, "mumbo" assert_raise(NoMethodError) { topic.mumbo } assert_raise(NoMethodError) { topic.mumbo = 5 } end test "undeclared attribute method does not affect respond_to? and method_missing" do topic = @target.new(title: "Budget") - assert topic.respond_to?("title") + assert_respond_to topic, "title" assert_equal "Budget", topic.title - assert !topic.respond_to?("title_hello_world") + assert_not_respond_to topic, "title_hello_world" assert_raise(NoMethodError) { topic.title_hello_world } end @@ -491,7 +478,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase @target.attribute_method_prefix prefix meth = "#{prefix}title" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -505,7 +492,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "Budget") meth = "title#{suffix}" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -519,7 +506,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "Budget") meth = "#{prefix}title#{suffix}" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -541,7 +528,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase else topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first end - assert !topic.is_test? + assert_not_predicate topic, :is_test? end test "typecast attribute from select to true" do @@ -552,7 +539,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase else topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first end - assert topic.is_test? + assert_predicate topic, :is_test? end test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do @@ -736,6 +723,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + test "setting invalid string to a zone-aware time attribute" do + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new + time_string = "ABC" + + record.bonus_time = time_string + assert_nil record.bonus_time + end + end + test "removing time zone-aware types" do with_time_zone_aware_types(:datetime) do in_time_zone "Pacific Time (US & Canada)" do @@ -743,7 +740,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase expected_time = Time.utc(2000, 01, 01, 10) assert_equal expected_time, record.bonus_time - assert record.bonus_time.utc? + assert_predicate record.bonus_time, :utc? end end end @@ -767,7 +764,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title") topic = @target.new(title: "The pros and cons of programming naked.") - assert !topic.respond_to?(:title) + assert_not_respond_to topic, :title exception = assert_raise(NoMethodError) { topic.title } assert_includes exception.message, "private method" assert_equal "I'm private", topic.send(:title) @@ -777,7 +774,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title=(value)") topic = @target.new - assert !topic.respond_to?(:title=) + assert_not_respond_to topic, :title= exception = assert_raise(NoMethodError) { topic.title = "Pants" } assert_includes exception.message, "private method" topic.send(:title=, "Very large pants") @@ -787,7 +784,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title?") topic = @target.new(title: "Isaac Newton's pants") - assert !topic.respond_to?(:title?) + assert_not_respond_to topic, :title? exception = assert_raise(NoMethodError) { topic.title? } assert_includes exception.message, "private method" assert topic.send(:title?) @@ -827,7 +824,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) computer = klass.new assert_nil computer.system end @@ -841,8 +838,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) - assert !subklass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) + assert_not subklass.instance_method_already_implemented?(:system) computer = subklass.new assert_nil computer.system end @@ -979,9 +976,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "came_from_user?" do model = @target.first - assert_not model.id_came_from_user? + assert_not_predicate model, :id_came_from_user? model.id = "omg" - assert model.id_came_from_user? + assert_predicate model, :id_came_from_user? end test "accessed_fields" do diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 8ebfee61ff..3bc56694be 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -211,7 +211,7 @@ module ActiveRecord end test "attributes not backed by database columns are not dirty when unchanged" do - refute OverloadedType.new.non_existent_decimal_changed? + assert_not_predicate OverloadedType.new, :non_existent_decimal_changed? end test "attributes not backed by database columns are always initialized" do @@ -245,13 +245,13 @@ module ActiveRecord model.foo << "asdf" assert_equal "lolasdf", model.foo - assert model.foo_changed? + assert_predicate model, :foo_changed? model.reload assert_equal "lol", model.foo model.foo = "lol" - refute model.changed? + assert_not_predicate model, :changed? end test "attributes not backed by database columns appear in inspect" do diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 4d8368fd8a..ade1f4b44d 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -27,6 +27,7 @@ require "models/member_detail" require "models/organization" require "models/guitar" require "models/tuning_peg" +require "models/reply" class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -50,8 +51,8 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase } 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? + u.update!(first_name: "nah") # still valid because validation only applies on 'create' + assert_predicate reference.create!(person: u), :persisted? end def test_should_not_add_the_same_callbacks_multiple_times_for_has_one @@ -74,7 +75,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase ship = ShipWithoutNestedAttributes.new ship.prisoners.build - assert_not ship.valid? + assert_not_predicate ship, :valid? assert_equal 1, ship.errors[:name].length end @@ -99,35 +100,35 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas def test_should_save_parent_but_not_invalid_child firm = Firm.new(name: "GlobalMegaCorp") - assert firm.valid? + assert_predicate firm, :valid? firm.build_account_using_primary_key - assert !firm.build_account_using_primary_key.valid? + assert_not_predicate firm.build_account_using_primary_key, :valid? assert firm.save - assert !firm.account_using_primary_key.persisted? + assert_not_predicate firm.account_using_primary_key, :persisted? end def test_save_fails_for_invalid_has_one firm = Firm.first - assert firm.valid? + assert_predicate firm, :valid? firm.build_account - assert !firm.account.valid? - assert !firm.valid? - assert !firm.save + assert_not_predicate firm.account, :valid? + assert_not_predicate firm, :valid? + assert_not firm.save assert_equal ["is invalid"], firm.errors["account"] end def test_save_succeeds_for_invalid_has_one_with_validate_false firm = Firm.first - assert firm.valid? + assert_predicate firm, :valid? firm.build_unvalidated_account - assert !firm.unvalidated_account.valid? - assert firm.valid? + assert_not_predicate firm.unvalidated_account, :valid? + assert_predicate firm, :valid? assert firm.save end @@ -136,10 +137,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas account = firm.build_account("credit_limit" => 1000) assert_equal account, firm.account - assert !account.persisted? + assert_not_predicate account, :persisted? assert firm.save assert_equal account, firm.account - assert account.persisted? + assert_predicate account, :persisted? end def test_build_before_either_saved @@ -147,16 +148,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm.account = account = Account.new("credit_limit" => 1000) assert_equal account, firm.account - assert !account.persisted? + assert_not_predicate account, :persisted? assert firm.save assert_equal account, firm.account - assert account.persisted? + assert_predicate account, :persisted? end def test_assignment_before_parent_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.find(1) - assert !firm.persisted? + assert_not_predicate firm, :persisted? assert_equal a, firm.account assert firm.save assert_equal a, firm.account @@ -167,12 +168,12 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas def test_assignment_before_either_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.new("credit_limit" => 1000) - assert !firm.persisted? - assert !a.persisted? + assert_not_predicate firm, :persisted? + assert_not_predicate a, :persisted? assert_equal a, firm.account assert firm.save - assert firm.persisted? - assert a.persisted? + assert_predicate firm, :persisted? + assert_predicate a, :persisted? assert_equal a, firm.account firm.association(:account).reload assert_equal a, firm.account @@ -221,13 +222,13 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test def test_should_save_parent_but_not_invalid_child client = Client.new(name: "Joe (the Plumber)") - assert client.valid? + assert_predicate client, :valid? client.build_firm - assert !client.firm.valid? + assert_not_predicate client.firm, :valid? assert client.save - assert !client.firm.persisted? + assert_not_predicate client.firm, :persisted? end def test_save_fails_for_invalid_belongs_to @@ -235,9 +236,9 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert log = AuditLog.create(developer_id: 0, message: " ") log.developer = Developer.new - assert !log.developer.valid? - assert !log.valid? - assert !log.save + assert_not_predicate log.developer, :valid? + assert_not_predicate log, :valid? + assert_not log.save assert_equal ["is invalid"], log.errors["developer"] end @@ -246,8 +247,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert log = AuditLog.create(developer_id: 0, message: " ") log.unvalidated_developer = Developer.new - assert !log.unvalidated_developer.valid? - assert log.valid? + assert_not_predicate log.unvalidated_developer, :valid? + assert_predicate log, :valid? assert log.save end @@ -256,10 +257,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm - assert !apple.persisted? + assert_not_predicate apple, :persisted? assert client.save assert apple.save - assert apple.persisted? + assert_predicate apple, :persisted? assert_equal apple, client.firm client.association(:firm).reload assert_equal apple, client.firm @@ -269,11 +270,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test final_cut = Client.new("name" => "Final Cut") apple = Firm.new("name" => "Apple") final_cut.firm = apple - assert !final_cut.persisted? - assert !apple.persisted? + assert_not_predicate final_cut, :persisted? + assert_not_predicate apple, :persisted? assert final_cut.save - assert final_cut.persisted? - assert apple.persisted? + assert_predicate final_cut, :persisted? + assert_predicate apple, :persisted? assert_equal apple, final_cut.firm final_cut.association(:firm).reload assert_equal apple, final_cut.firm @@ -382,7 +383,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test auditlog.developer = invalid_developer auditlog.developer_id = valid_developer.id - assert auditlog.valid? + assert_predicate auditlog, :valid? end end @@ -395,8 +396,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] molecule.save - assert_not invalid_electron.valid? - assert valid_electron.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid" end @@ -408,9 +409,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] - assert_not tuning_peg_invalid.valid? - assert tuning_peg_valid.valid? - assert_not guitar.valid? + assert_not_predicate tuning_peg_invalid, :valid? + assert_predicate tuning_peg_valid, :valid? + assert_not_predicate guitar, :valid? assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"] assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"] end @@ -425,9 +426,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal ["can't be blank"], molecule.errors["electrons[1].name"] assert_not_equal ["can't be blank"], molecule.errors["electrons.name"] ensure @@ -441,9 +442,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"] end @@ -455,9 +456,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] - assert_not tuning_peg_invalid.valid? - assert tuning_peg_valid.valid? - assert_not guitar.valid? + assert_not_predicate tuning_peg_invalid, :valid? + assert_predicate tuning_peg_valid, :valid? + assert_not_predicate guitar, :valid? assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"] assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"] end @@ -472,9 +473,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"] assert_equal [], molecule.errors.details[:"electrons.name"] ensure @@ -488,8 +489,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron] molecule.save - assert valid_electron.valid? - assert molecule.persisted? + assert_predicate valid_electron, :valid? + assert_predicate molecule, :persisted? assert_equal 1, molecule.electrons.count end end @@ -499,22 +500,34 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_invalid_adding firm = Firm.find(1) - assert !(firm.clients_of_firm << c = Client.new) - assert !c.persisted? - assert !firm.valid? - assert !firm.save - assert !c.persisted? + assert_not (firm.clients_of_firm << c = Client.new) + assert_not_predicate c, :persisted? + assert_not_predicate firm, :valid? + assert_not firm.save + assert_not_predicate c, :persisted? end def test_invalid_adding_before_save new_firm = Firm.new("name" => "A New Firm, Inc") new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) - assert !c.persisted? - assert !c.valid? - assert !new_firm.valid? - assert !new_firm.save - assert !c.persisted? - assert !new_firm.persisted? + assert_not_predicate c, :persisted? + assert_not_predicate c, :valid? + assert_not_predicate new_firm, :valid? + assert_not new_firm.save + assert_not_predicate c, :persisted? + assert_not_predicate new_firm, :persisted? + end + + def test_adding_unsavable_association + new_firm = Firm.new("name" => "A New Firm, Inc") + client = new_firm.clients.new("name" => "Apple") + client.throw_on_save = true + + assert_predicate client, :valid? + assert_predicate new_firm, :valid? + assert_not new_firm.save + assert_not_predicate new_firm, :persisted? + assert_not_predicate client, :persisted? end def test_invalid_adding_with_validate_false @@ -522,10 +535,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa client = Client.new firm.unvalidated_clients_of_firm << client - assert firm.valid? - assert !client.valid? + assert_predicate firm, :valid? + assert_not_predicate client, :valid? assert firm.save - assert !client.persisted? + assert_not_predicate client, :persisted? end def test_valid_adding_with_validate_false @@ -534,24 +547,39 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa firm = Firm.first client = Client.new("name" => "Apple") - assert firm.valid? - assert client.valid? - assert !client.persisted? + assert_predicate firm, :valid? + assert_predicate client, :valid? + assert_not_predicate client, :persisted? firm.unvalidated_clients_of_firm << client assert firm.save - assert client.persisted? + assert_predicate client, :persisted? assert_equal no_of_clients + 1, Client.count end + def test_parent_should_not_get_saved_with_duplicate_children_records + assert_no_difference "Reply.count" do + assert_no_difference "SillyUniqueReply.count" do + reply = Reply.new + reply.silly_unique_replies.build([ + { content: "Best content" }, + { content: "Best content" } + ]) + + assert_not reply.save + assert_not_empty reply.errors + end + end + end + def test_invalid_build new_client = companies(:first_firm).clients_of_firm.build - assert !new_client.persisted? - assert !new_client.valid? + assert_not_predicate new_client, :persisted? + assert_not_predicate new_client, :valid? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert !companies(:first_firm).save - assert !new_client.persisted? + assert_not companies(:first_firm).save + assert_not_predicate new_client, :persisted? assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end @@ -570,8 +598,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_firms, Firm.count # Firm was not saved to database. assert_equal no_of_clients, Client.count # Clients were not saved to database. assert new_firm.save - assert new_firm.persisted? - assert c.persisted? + assert_predicate new_firm, :persisted? + assert_predicate c, :persisted? assert_equal new_firm, c.firm assert_equal no_of_firms + 1, Firm.count # Firm was saved to database. assert_equal no_of_clients + 2, Client.count # Clients were saved to database. @@ -601,11 +629,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" assert_queries(2) { assert company.save } - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal 3, company.clients_of_firm.reload.size end @@ -621,11 +649,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" assert_queries(2) { assert company.save } - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal 3, company.clients_of_firm.reload.size end @@ -657,62 +685,62 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase new_account = Account.new("credit_limit" => 1000) new_firm = Firm.new("name" => "some firm") - assert !new_firm.persisted? + assert_not_predicate new_firm, :persisted? new_account.firm = new_firm new_account.save! - assert new_firm.persisted? + assert_predicate new_firm, :persisted? new_account = Account.new("credit_limit" => 1000) new_autosaved_firm = Firm.new("name" => "some firm") - assert !new_autosaved_firm.persisted? + assert_not_predicate new_autosaved_firm, :persisted? new_account.unautosaved_firm = new_autosaved_firm new_account.save! - assert !new_autosaved_firm.persisted? + assert_not_predicate new_autosaved_firm, :persisted? end def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.account = account firm.save! - assert account.persisted? + assert_predicate account, :persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) firm.unautosaved_account = account - assert !account.persisted? + assert_not_predicate account, :persisted? firm.unautosaved_account = account firm.save! - assert !account.persisted? + assert_not_predicate account, :persisted? end def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.accounts << account firm.save! - assert account.persisted? + assert_predicate account, :persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.unautosaved_accounts << account firm.save! - assert !account.persisted? + assert_not_predicate account, :persisted? end def test_autosave_new_record_with_after_create_callback @@ -745,18 +773,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.mark_for_destruction @pirate.ship.mark_for_destruction - assert !@pirate.reload.marked_for_destruction? - assert !@pirate.ship.reload.marked_for_destruction? + assert_not_predicate @pirate.reload, :marked_for_destruction? + assert_not_predicate @pirate.ship.reload, :marked_for_destruction? end # has_one def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction - assert !@pirate.ship.marked_for_destruction? + assert_not_predicate @pirate.ship, :marked_for_destruction? @pirate.ship.mark_for_destruction id = @pirate.ship.id - assert @pirate.ship.marked_for_destruction? + assert_predicate @pirate.ship, :marked_for_destruction? assert Ship.find_by_id(id) @pirate.save @@ -766,11 +794,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_skip_validation_on_a_child_association_if_marked_for_destruction @pirate.ship.name = "" - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.ship.mark_for_destruction - @pirate.ship.expects(:valid?).never - assert_difference("Ship.count", -1) { @pirate.save! } + assert_not_called(@pirate.ship, :valid?) do + assert_difference("Ship.count", -1) { @pirate.save! } + end end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice @@ -795,7 +824,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" @ship.name_will_change! - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_not_nil @pirate.reload.ship end @@ -806,18 +835,19 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved - @pirate.ship.expects(:save).never - assert @pirate.save + assert_not_called(@pirate.ship, :save) do + assert @pirate.save + end end # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction - assert !@ship.pirate.marked_for_destruction? + assert_not_predicate @ship.pirate, :marked_for_destruction? @ship.pirate.mark_for_destruction id = @ship.pirate.id - assert @ship.pirate.marked_for_destruction? + assert_predicate @ship.pirate, :marked_for_destruction? assert Pirate.find_by_id(id) @ship.save @@ -827,11 +857,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction @ship.pirate.catchphrase = "" - assert !@ship.valid? + assert_not_predicate @ship, :valid? @ship.pirate.mark_for_destruction - @ship.pirate.expects(:valid?).never - assert_difference("Pirate.count", -1) { @ship.save! } + assert_not_called(@ship.pirate, :valid?) do + assert_difference("Pirate.count", -1) { @ship.save! } + end end def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice @@ -855,7 +886,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_not_nil @ship.reload.pirate end @@ -871,7 +902,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") } - assert !@pirate.birds.any?(&:marked_for_destruction?) + assert_not @pirate.birds.any?(&:marked_for_destruction?) @pirate.birds.each(&:mark_for_destruction) klass = @pirate.birds.first.class @@ -881,7 +912,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase ids.each { |id| assert klass.find_by_id(id) } @pirate.save - assert @pirate.reload.birds.empty? + assert_empty @pirate.reload.birds ids.each { |id| assert_nil klass.find_by_id(id) } end @@ -889,30 +920,32 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.create!(name: :parrot) @pirate.birds.first.destroy @pirate.save! - assert @pirate.reload.birds.empty? + assert_empty @pirate.reload.birds end def test_should_skip_validation_on_has_many_if_marked_for_destruction 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") } @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? - @pirate.birds.each do |bird| - bird.mark_for_destruction - bird.expects(:valid?).never + @pirate.birds.each(&:mark_for_destruction) + + assert_not_called(@pirate.birds.first, :valid?) do + assert_not_called(@pirate.birds.last, :valid?) do + assert_difference("Bird.count", -2) { @pirate.save! } + end end - assert_difference("Bird.count", -2) { @pirate.save! } end def test_should_skip_validation_on_has_many_if_destroyed @pirate.birds.create!(name: "birds_1") @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.birds.each(&:destroy) - assert @pirate.valid? + assert_predicate @pirate, :valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many @@ -921,8 +954,11 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.each(&:mark_for_destruction) assert @pirate.save - @pirate.birds.each { |bird| bird.expects(:destroy).never } - assert @pirate.save + @pirate.birds.each do |bird| + assert_not_called(bird, :destroy) do + assert @pirate.save + end + end end def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many @@ -937,7 +973,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.birds end @@ -1003,42 +1039,44 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") } - assert !@pirate.parrots.any?(&:marked_for_destruction?) + assert_not @pirate.parrots.any?(&:marked_for_destruction?) @pirate.parrots.each(&:mark_for_destruction) assert_no_difference "Parrot.count" do @pirate.save end - assert @pirate.reload.parrots.empty? + assert_empty @pirate.reload.parrots join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}") - assert join_records.empty? + assert_empty join_records end def test_should_skip_validation_on_habtm_if_marked_for_destruction 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") } @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? - @pirate.parrots.each do |parrot| - parrot.mark_for_destruction - parrot.expects(:valid?).never + @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + + assert_not_called(@pirate.parrots.first, :valid?) do + assert_not_called(@pirate.parrots.last, :valid?) do + @pirate.save! + end end - @pirate.save! - assert @pirate.reload.parrots.empty? + assert_empty @pirate.reload.parrots end def test_should_skip_validation_on_habtm_if_destroyed @pirate.parrots.create!(name: "parrots_1") @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.parrots.each(&:destroy) - assert @pirate.valid? + assert_predicate @pirate, :valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm @@ -1065,7 +1103,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.parrots end @@ -1145,16 +1183,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @pirate.ship.name = "" - assert @pirate.invalid? - assert @pirate.errors[:"ship.name"].any? + assert_predicate @pirate, :invalid? + assert_predicate @pirate.errors[:"ship.name"], :any? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @pirate.ship.name = nil @pirate.catchphrase = nil - assert @pirate.invalid? - assert @pirate.errors[:"ship.name"].any? - assert @pirate.errors[:catchphrase].any? + assert_predicate @pirate, :invalid? + assert_predicate @pirate.errors[:"ship.name"], :any? + assert_predicate @pirate.errors[:catchphrase], :any? end def test_should_not_ignore_different_error_messages_on_the_same_attribute @@ -1163,7 +1201,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase Ship.validates_format_of :name, with: /\w/ @pirate.ship.name = "" @pirate.catchphrase = nil - assert @pirate.invalid? + assert_predicate @pirate, :invalid? assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"] ensure Ship._validators = old_validators if old_validators @@ -1213,7 +1251,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_no_difference "Pirate.count" do assert_no_difference "Ship.count" do - assert !pirate.save + assert_not pirate.save end end end @@ -1232,7 +1270,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name] end @@ -1244,7 +1282,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") ship.parts.build.mark_for_destruction - assert_not ship.valid? + assert_not_predicate ship, :valid? end end @@ -1299,16 +1337,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @ship.pirate.catchphrase = "" - assert @ship.invalid? - assert @ship.errors[:"pirate.catchphrase"].any? + assert_predicate @ship, :invalid? + assert_predicate @ship.errors[:"pirate.catchphrase"], :any? end def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid @ship.name = nil @ship.pirate.catchphrase = nil - assert @ship.invalid? - assert @ship.errors[:name].any? - assert @ship.errors[:"pirate.catchphrase"].any? + assert_predicate @ship, :invalid? + assert_predicate @ship.errors[:name], :any? + assert_predicate @ship.errors[:"pirate.catchphrase"], :any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -1337,7 +1375,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase assert_no_difference "Ship.count" do assert_no_difference "Pirate.count" do - assert !ship.save + assert_not ship.save end end end @@ -1356,7 +1394,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name] end @@ -1403,17 +1441,17 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] end def test_should_not_use_default_invalid_error_on_associated_models @pirate.send(@association_name).build(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] end def test_should_default_invalid_error_from_i18n @@ -1423,10 +1461,10 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).build(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"] assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] ensure I18n.backend = I18n::Backend::Simple.new end @@ -1435,9 +1473,9 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each { |child| child.name = "" } @pirate.catchphrase = nil - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[:catchphrase].any? + assert_predicate @pirate.errors[:catchphrase], :any? end def test_should_allow_to_bypass_validations_on_the_associated_models_on_update @@ -1480,7 +1518,7 @@ module AutosaveAssociationOnACollectionAssociationTests @child_1.name = "Changed" @child_1.cancel_save_from_callback = true - assert !@pirate.save + assert_not @pirate.save assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase assert_equal "Posideons Killer", @child_1.reload.name @@ -1490,7 +1528,7 @@ module AutosaveAssociationOnACollectionAssociationTests assert_no_difference "Pirate.count" do assert_no_difference "#{new_child.class.name}.count" do - assert !new_pirate.save + assert_not new_pirate.save end end end @@ -1510,7 +1548,7 @@ module AutosaveAssociationOnACollectionAssociationTests end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)] end @@ -1595,10 +1633,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te end test "should automatically validate associations" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end end @@ -1613,15 +1651,15 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.ship.name = "" - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically add validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_ship.name = "" - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1634,15 +1672,15 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.parrot = Parrot.new(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_parrot = Parrot.new(name: "") - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1655,17 +1693,17 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.parrots = [ Parrot.new(name: "popuga") ] @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ] @pirate.non_validated_parrots.each { |parrot| parrot.name = "" } - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1686,7 +1724,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas end test "should not generate validation methods for has_one associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship) + assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship end test "should generate validation methods for belongs_to associations with :validate => true" do @@ -1694,7 +1732,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas end test "should not generate validation methods for belongs_to associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot) + assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot end test "should generate validation methods for HABTM associations with :validate => true" do diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a49990008c..d216fe16fa 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -14,7 +14,6 @@ require "models/computer" require "models/project" require "models/default" require "models/auto_id" -require "models/boolean" require "models/column_name" require "models/subscriber" require "models/comment" @@ -147,8 +146,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_table_exists - assert !NonExistentTable.table_exists? - assert Topic.table_exists? + assert_not_predicate NonExistentTable, :table_exists? + assert_predicate Topic, :table_exists? end def test_preserving_date_objects @@ -307,7 +306,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "Dude", cbs[0].name assert_equal "Bob", cbs[1].name assert cbs[0].frickinawesome - assert !cbs[1].frickinawesome + assert_not cbs[1].frickinawesome end def test_load @@ -450,7 +449,7 @@ class BasicsTest < ActiveRecord::TestCase def test_default_values topic = Topic.new - assert topic.approved? + assert_predicate topic, :approved? assert_nil topic.written_on assert_nil topic.bonus_time assert_nil topic.last_read @@ -458,7 +457,7 @@ class BasicsTest < ActiveRecord::TestCase topic.save topic = Topic.find(topic.id) - assert topic.approved? + assert_predicate topic, :approved? assert_nil topic.last_read # Oracle has some funky default handling, so it requires a bit of @@ -716,48 +715,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal expected_attributes, category.attributes end - def test_boolean - b_nil = Boolean.create("value" => nil) - nil_id = b_nil.id - b_false = Boolean.create("value" => false) - false_id = b_false.id - b_true = Boolean.create("value" => true) - true_id = b_true.id - - b_nil = Boolean.find(nil_id) - assert_nil b_nil.value - b_false = Boolean.find(false_id) - assert !b_false.value? - b_true = Boolean.find(true_id) - assert b_true.value? - end - - def test_boolean_without_questionmark - b_true = Boolean.create("value" => true) - true_id = b_true.id - - subclass = Class.new(Boolean).find true_id - superclass = Boolean.find true_id - - assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun) - end - - def test_boolean_cast_from_string - b_blank = Boolean.create("value" => "") - blank_id = b_blank.id - b_false = Boolean.create("value" => "0") - false_id = b_false.id - b_true = Boolean.create("value" => "1") - true_id = b_true.id - - b_blank = Boolean.find(blank_id) - assert_nil b_blank.value - b_false = Boolean.find(false_id) - assert !b_false.value? - b_true = Boolean.find(true_id) - assert b_true.value? - end - def test_new_record_returns_boolean assert_equal false, Topic.new.persisted? assert_equal true, Topic.find(1).persisted? @@ -768,7 +725,7 @@ class BasicsTest < ActiveRecord::TestCase duped_topic = nil assert_nothing_raised { duped_topic = topic.dup } assert_equal topic.title, duped_topic.title - assert !duped_topic.persisted? + assert_not_predicate duped_topic, :persisted? # test if the attributes have been duped topic.title = "a" @@ -786,7 +743,7 @@ class BasicsTest < ActiveRecord::TestCase # test if saved clone object differs from original duped_topic.save - assert duped_topic.persisted? + assert_predicate duped_topic, :persisted? assert_not_equal duped_topic.id, topic.id duped_topic.reload @@ -807,7 +764,7 @@ class BasicsTest < ActiveRecord::TestCase assert_nothing_raised { dup = dev.dup } assert_kind_of DeveloperSalary, dup.salary assert_equal dev.salary.amount, dup.salary.amount - assert !dup.persisted? + assert_not_predicate dup, :persisted? # test if the attributes have been duped original_amount = dup.salary.amount @@ -815,7 +772,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal original_amount, dup.salary.amount assert dup.save - assert dup.persisted? + assert_predicate dup, :persisted? assert_not_equal dup.id, dev.id end @@ -835,52 +792,52 @@ class BasicsTest < ActiveRecord::TestCase def test_clone_of_new_object_with_defaults developer = Developer.new - assert !developer.name_changed? - assert !developer.salary_changed? + assert_not_predicate developer, :name_changed? + assert_not_predicate developer, :salary_changed? cloned_developer = developer.clone - assert !cloned_developer.name_changed? - assert !cloned_developer.salary_changed? + assert_not_predicate cloned_developer, :name_changed? + assert_not_predicate cloned_developer, :salary_changed? end def test_clone_of_new_object_marks_attributes_as_dirty developer = Developer.new name: "Bjorn", salary: 100000 - assert developer.name_changed? - assert developer.salary_changed? + assert_predicate developer, :name_changed? + assert_predicate developer, :salary_changed? cloned_developer = developer.clone - assert cloned_developer.name_changed? - assert cloned_developer.salary_changed? + assert_predicate cloned_developer, :name_changed? + assert_predicate cloned_developer, :salary_changed? end def test_clone_of_new_object_marks_as_dirty_only_changed_attributes developer = Developer.new name: "Bjorn" assert developer.name_changed? # obviously - assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed + assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed cloned_developer = developer.clone - assert cloned_developer.name_changed? - assert !cloned_developer.salary_changed? # ... and cloned instance should behave same + assert_predicate cloned_developer, :name_changed? + assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same end def test_dup_of_saved_object_marks_attributes_as_dirty developer = Developer.create! name: "Bjorn", salary: 100000 - assert !developer.name_changed? - assert !developer.salary_changed? + assert_not_predicate developer, :name_changed? + assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # both attributes differ from defaults - assert cloned_developer.salary_changed? + assert_predicate cloned_developer, :salary_changed? end def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! name: "Bjorn" - assert !developer.name_changed? # both attributes of saved object should be treated as not changed - assert !developer.salary_changed? + assert_not developer.name_changed? # both attributes of saved object should be treated as not changed + assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be - assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance + assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance end def test_bignum @@ -951,14 +908,14 @@ class BasicsTest < ActiveRecord::TestCase end def test_toggle_attribute - assert !topics(:first).approved? + assert_not_predicate topics(:first), :approved? topics(:first).toggle!(:approved) - assert topics(:first).approved? + assert_predicate topics(:first), :approved? topic = topics(:first) topic.toggle(:approved) - assert !topic.approved? + assert_not_predicate topic, :approved? topic.reload - assert topic.approved? + assert_predicate topic, :approved? end def test_reload @@ -982,7 +939,7 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_clear_cash_when_setting_table_name + def test_clear_cache_when_setting_table_name original_table_name = Joke.table_name Joke.table_name = "funny_jokes" @@ -1431,11 +1388,11 @@ class BasicsTest < ActiveRecord::TestCase test "resetting column information doesn't remove attribute methods" do topic = topics(:first) - assert_not topic.id_changed? + assert_not_predicate topic, :id_changed? Topic.reset_column_information - assert_not topic.id_changed? + assert_not_predicate topic, :id_changed? end test "ignored columns are not present in columns_hash" do @@ -1447,27 +1404,27 @@ class BasicsTest < ActiveRecord::TestCase end test "ignored columns have no attribute methods" do - refute Developer.new.respond_to?(:first_name) - refute Developer.new.respond_to?(:first_name=) - refute Developer.new.respond_to?(:first_name?) - refute SubDeveloper.new.respond_to?(:first_name) - refute SubDeveloper.new.respond_to?(:first_name=) - refute SubDeveloper.new.respond_to?(:first_name?) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name=) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name?) + assert_not_respond_to Developer.new, :first_name + assert_not_respond_to Developer.new, :first_name= + assert_not_respond_to Developer.new, :first_name? + assert_not_respond_to SubDeveloper.new, :first_name + assert_not_respond_to SubDeveloper.new, :first_name= + assert_not_respond_to SubDeveloper.new, :first_name? + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name= + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name? end test "ignored columns don't prevent explicit declaration of attribute methods" do - assert Developer.new.respond_to?(:last_name) - assert Developer.new.respond_to?(:last_name=) - assert Developer.new.respond_to?(:last_name?) - assert SubDeveloper.new.respond_to?(:last_name) - assert SubDeveloper.new.respond_to?(:last_name=) - assert SubDeveloper.new.respond_to?(:last_name?) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name=) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name?) + assert_respond_to Developer.new, :last_name + assert_respond_to Developer.new, :last_name= + assert_respond_to Developer.new, :last_name? + assert_respond_to SubDeveloper.new, :last_name + assert_respond_to SubDeveloper.new, :last_name= + assert_respond_to SubDeveloper.new, :last_name? + assert_respond_to SymbolIgnoredDeveloper.new, :last_name + assert_respond_to SymbolIgnoredDeveloper.new, :last_name= + assert_respond_to SymbolIgnoredDeveloper.new, :last_name? end test "ignored columns are stored as an array of string" do @@ -1477,31 +1434,31 @@ class BasicsTest < ActiveRecord::TestCase test "when #reload called, ignored columns' attribute methods are not defined" do developer = Developer.create!(name: "Developer") - refute developer.respond_to?(:first_name) - refute developer.respond_to?(:first_name=) + assert_not_respond_to developer, :first_name + assert_not_respond_to developer, :first_name= developer.reload - refute developer.respond_to?(:first_name) - refute developer.respond_to?(:first_name=) + assert_not_respond_to developer, :first_name + assert_not_respond_to developer, :first_name= end test "ignored columns not included in SELECT" do query = Developer.all.to_sql.downcase # ignored column - refute query.include?("first_name") + assert_not query.include?("first_name") # regular column assert query.include?("name") end test "column names are quoted when using #from clause and model has ignored columns" do - refute_empty Developer.ignored_columns + assert_not_empty Developer.ignored_columns query = Developer.from("developers").to_sql quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" - assert_match(/SELECT #{quoted_id}.* FROM developers/, query) + assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query) end test "using table name qualified column names unless having SELECT list explicitly" do diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index be8aeed5ac..c8163901c6 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -313,7 +313,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_each_record_should_yield_record_if_block_is_given assert_queries(6) do Post.in_batches(of: 2).each_record do |post| - assert post.title.present? + assert_predicate post.title, :present? assert_kind_of Post, post end end @@ -322,7 +322,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_each_record_should_return_enumerator_if_no_block_given assert_queries(6) do Post.in_batches(of: 2).each_record.with_index do |post, i| - assert post.title.present? + assert_predicate post.title, :present? assert_kind_of Post, post end end @@ -353,24 +353,24 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_be_loaded Post.in_batches(of: 1) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end Post.in_batches(of: 1, load: false) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end end def test_in_batches_should_be_loaded Post.in_batches(of: 1, load: true) do |relation| - assert relation.loaded? + assert_predicate relation, :loaded? end end def test_in_batches_if_not_loaded_executes_more_queries assert_queries(@total + 1) do Post.in_batches(of: 1, load: false) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end end end @@ -508,7 +508,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches Post.update_all(author_id: 0) person = Post.last - person.update_attributes(author_id: 1) + person.update(author_id: 1) Post.in_batches(of: 2) do |batch| batch.where("author_id >= 1").update_all("author_id = author_id + 1") @@ -592,7 +592,11 @@ class EachTest < ActiveRecord::TestCase table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - posts = ActiveRecord::Relation.create(Post, table_alias, predicate_builder) + posts = ActiveRecord::Relation.create( + Post, + table: table_alias, + predicate_builder: predicate_builder + ) posts.find_each {} end end diff --git a/activerecord/test/cases/boolean_test.rb b/activerecord/test/cases/boolean_test.rb new file mode 100644 index 0000000000..ab9f974e2c --- /dev/null +++ b/activerecord/test/cases/boolean_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/boolean" + +class BooleanTest < ActiveRecord::TestCase + def test_boolean + b_nil = Boolean.create!(value: nil) + b_false = Boolean.create!(value: false) + b_true = Boolean.create!(value: true) + + assert_nil Boolean.find(b_nil.id).value + assert_not_predicate Boolean.find(b_false.id), :value? + assert_predicate Boolean.find(b_true.id), :value? + end + + def test_boolean_without_questionmark + b_true = Boolean.create!(value: true) + + subclass = Class.new(Boolean).find(b_true.id) + superclass = Boolean.find(b_true.id) + + assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun) + end + + def test_boolean_cast_from_string + b_blank = Boolean.create!(value: "") + b_false = Boolean.create!(value: "0") + b_true = Boolean.create!(value: "1") + + assert_nil Boolean.find(b_blank.id).value + assert_not_predicate Boolean.find(b_false.id), :value? + assert_predicate Boolean.find(b_true.id), :value? + end + + def test_find_by_boolean_string + b_false = Boolean.create!(value: "false") + b_true = Boolean.create!(value: "true") + + assert_equal b_false, Boolean.find_by(value: "false") + assert_equal b_true, Boolean.find_by(value: "true") + end +end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 8f2f2c6186..3a569f226e 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -38,8 +38,8 @@ module ActiveRecord end test "cache_version is only there when versioning is on" do - assert CacheMeWithVersion.create.cache_version.present? - assert_not CacheMe.create.cache_version.present? + assert_predicate CacheMeWithVersion.create.cache_version, :present? + assert_not_predicate CacheMe.create.cache_version, :present? end test "cache_key_with_version always has both key and version" do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5eda39a0c7..5c9ed42173 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -21,7 +21,7 @@ require "models/comment" require "models/rating" class CalculationsTest < ActiveRecord::TestCase - fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books + fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments def test_should_sum_field assert_equal 318, Account.sum(:credit_limit) @@ -236,6 +236,18 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_count_with_eager_loading_and_custom_order + posts = Post.includes(:comments).order("comments.id") + assert_queries(1) { assert_equal 11, posts.count } + assert_queries(1) { assert_equal 11, posts.count(:all) } + end + + def test_count_with_eager_loading_and_custom_order_and_distinct + posts = Post.includes(:comments).order("comments.id").distinct + assert_queries(1) { assert_equal 11, posts.count } + assert_queries(1) { assert_equal 11, posts.count(:all) } + end + def test_distinct_count_all_with_custom_select_and_order accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10")) assert_queries(1) { assert_equal 3, accounts.count(:all) } @@ -630,6 +642,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [ topic.written_on ], relation.pluck(:written_on) end + def test_pluck_with_type_cast_does_not_corrupt_the_query_cache + topic = topics(:first) + relation = Topic.where(id: topic.id) + assert_queries 1 do + Topic.cache do + kind = relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on).class + relation.pluck(:written_on) + assert_kind_of kind, relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on) + end + end + end + def test_pluck_and_distinct assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit) end @@ -688,6 +712,29 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id) end + def test_pluck_with_includes_offset + assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id) + assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id) + end + + def test_group_by_with_limit + expected = { "Post" => 8, "SpecialPost" => 1 } + actual = Post.includes(:comments).group(:type).order(:type).limit(2).count("comments.id") + assert_equal expected, actual + end + + def test_group_by_with_offset + expected = { "SpecialPost" => 1, "StiPost" => 2 } + actual = Post.includes(:comments).group(:type).order(:type).offset(1).count("comments.id") + assert_equal expected, actual + end + + def test_group_by_with_limit_and_offset + expected = { "SpecialPost" => 1 } + actual = Post.includes(:comments).group(:type).order(:type).offset(1).limit(1).count("comments.id") + assert_equal expected, actual + end + def test_pluck_not_auto_table_name_prefix_if_column_included Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)]) ids = Company.includes(:contracts).pluck(:developer_id) @@ -782,6 +829,23 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_pick_one + assert_equal "The First Topic", Topic.order(:id).pick(:heading) + assert_nil Topic.none.pick(:heading) + assert_nil Topic.where("1=0").pick(:heading) + end + + def test_pick_two + assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address) + assert_nil Topic.none.pick(:author_name, :author_email_address) + assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) + end + + def test_pick_delegate_to_all + cool_first = minivans(:cool_first) + assert_equal cool_first.color, Minivan.pick(:color) + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 55c7475f46..253c3099d6 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -385,60 +385,60 @@ class CallbacksTest < ActiveRecord::TestCase end def assert_save_callbacks_not_called(someone) - assert !someone.after_save_called - assert !someone.after_create_called - assert !someone.after_update_called + assert_not someone.after_save_called + assert_not someone.after_create_called + assert_not someone.after_update_called end private :assert_save_callbacks_not_called def test_before_create_throwing_abort someone = CallbackHaltedDeveloper.new someone.cancel_before_create = true - assert someone.valid? - assert !someone.save + assert_predicate someone, :valid? + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_save_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) - assert david.valid? - assert !david.save + assert_predicate david, :valid? + assert_not david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 - assert !david.valid? - assert !david.save + assert_not_predicate david, :valid? + assert_not david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_save = true - assert someone.valid? - assert !someone.save + assert_predicate someone, :valid? + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_update_throwing_abort someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_update = true - assert someone.valid? - assert !someone.save + assert_predicate someone, :valid? + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_destroy_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) - assert !david.destroy + assert_not david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_destroy = true - assert !someone.destroy + assert_not someone.destroy assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - assert !someone.after_destroy_called + assert_not someone.after_destroy_called end def test_callback_throwing_abort @@ -467,13 +467,40 @@ class CallbacksTest < ActiveRecord::TestCase def test_inheritance_of_callbacks parent = ParentDeveloper.new - assert !parent.after_save_called + assert_not parent.after_save_called parent.save assert parent.after_save_called child = ChildDeveloper.new - assert !child.after_save_called + assert_not child.after_save_called child.save assert child.after_save_called end + + def test_before_save_doesnt_allow_on_option + exception = assert_raises ArgumentError do + Class.new(ActiveRecord::Base) do + before_save(on: :create) {} + end + end + assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message + end + + def test_around_save_doesnt_allow_on_option + exception = assert_raises ArgumentError do + Class.new(ActiveRecord::Base) do + around_save(on: :create) {} + end + end + assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message + end + + def test_after_save_doesnt_allow_on_option + exception = assert_raises ArgumentError do + Class.new(ActiveRecord::Base) do + after_save(on: :create) {} + end + end + assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message + end end diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb index 3187e6aed5..eea36ee736 100644 --- a/activerecord/test/cases/clone_test.rb +++ b/activerecord/test/cases/clone_test.rb @@ -12,7 +12,7 @@ module ActiveRecord cloned = topic.clone assert topic.persisted?, "topic persisted" assert cloned.persisted?, "topic persisted" - assert !cloned.new_record?, "topic is not new" + assert_not cloned.new_record?, "topic is not new" end def test_stays_frozen @@ -21,7 +21,7 @@ module ActiveRecord cloned = topic.clone assert cloned.persisted?, "topic persisted" - assert !cloned.new_record?, "topic is not new" + assert_not cloned.new_record?, "topic is not new" assert cloned.frozen?, "topic should be frozen" end @@ -36,7 +36,7 @@ module ActiveRecord cloned = Topic.new clone = cloned.clone cloned.freeze - assert_not clone.frozen? + assert_not_predicate clone, :frozen? end end end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index cfe95b2360..a5d908344a 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -60,7 +60,11 @@ module ActiveRecord table_metadata = ActiveRecord::TableMetadata.new(Developer, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - developers = ActiveRecord::Relation.create(Developer, table_alias, predicate_builder) + developers = ActiveRecord::Relation.create( + Developer, + table: table_alias, + predicate_builder: predicate_builder + ) developers = developers.where(salary: 100000).order(updated_at: :desc) last_developer_timestamp = developers.first.updated_at diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 82c6cf8dea..72838ff56b 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -45,11 +45,11 @@ module ActiveRecord # Make sure the pool marks the connection in use assert_equal @adapter, pool.connection - assert @adapter.in_use? + assert_predicate @adapter, :in_use? # Close should put the adapter back in the pool @adapter.close - assert_not @adapter.in_use? + assert_not_predicate @adapter, :in_use? assert_equal @adapter, pool.connection end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 603ed63a8c..b8e623f17b 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -71,6 +71,56 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + unless in_memory_db? + def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + }, + "another_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" + end + + def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "adapter" => "sqlite3", "database" => "db/primary.sqlite3" + }, + "another_env" => { + "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" + end + end + def test_establish_connection_using_two_level_configurations config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config @@ -103,11 +153,11 @@ module ActiveRecord end def test_active_connections? - assert !@handler.active_connections? + assert_not_predicate @handler, :active_connections? assert @handler.retrieve_connection(@spec_name) - assert @handler.active_connections? + assert_predicate @handler, :active_connections? @handler.clear_active_connections! - assert !@handler.active_connections? + assert_not_predicate @handler, :active_connections? end def test_retrieve_connection_pool @@ -146,7 +196,7 @@ module ActiveRecord def test_forked_child_doesnt_mangle_parent_connection object_id = ActiveRecord::Base.connection.object_id - assert ActiveRecord::Base.connection.active? + assert_predicate ActiveRecord::Base.connection, :active? rd, wr = IO.pipe rd.binmode @@ -171,6 +221,48 @@ module ActiveRecord assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people") end + unless in_memory_db? + def test_forked_child_recovers_from_disconnected_parent + object_id = ActiveRecord::Base.connection.object_id + assert_predicate ActiveRecord::Base.connection, :active? + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + outer_pid = fork { + ActiveRecord::Base.connection.disconnect! + + pid = fork { + rd.close + if ActiveRecord::Base.connection.active? + pair = [ActiveRecord::Base.connection.object_id, + ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")] + wr.write Marshal.dump pair + end + wr.close + + exit # allow finalizers to run + } + + Process.waitpid pid + } + + wr.close + + Process.waitpid outer_pid + child_id, child_count = Marshal.load(rd.read) + + assert_not_equal object_id, child_id + rd.close + + assert_equal 3, child_count + + # Outer connection is unaffected + assert_equal 6, ActiveRecord::Base.connection.select_value("SELECT 2 * COUNT(*) FROM people") + end + end + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool @pool.schema_cache = @pool.connection.schema_cache @pool.schema_cache.add("posts") @@ -236,7 +328,7 @@ module ActiveRecord pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) assert_same klass2.connection, pool.connection - refute_same klass2.connection, ActiveRecord::Base.connection + assert_not_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection @@ -255,7 +347,7 @@ module ActiveRecord def test_remove_connection_should_not_remove_parent klass2 = Class.new(Base) { def self.name; "klass2"; end } klass2.remove_connection - refute_nil ActiveRecord::Base.connection + assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection 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 006be9e65d..67496381d1 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -22,8 +22,8 @@ module ActiveRecord new_cache = YAML.load(YAML.dump(@cache)) assert_no_queries do - assert_equal 11, new_cache.columns("posts").size - assert_equal 11, new_cache.columns_hash("posts").size + assert_equal 12, new_cache.columns("posts").size + assert_equal 12, new_cache.columns_hash("posts").size assert new_cache.data_sources("posts") assert_equal "id", new_cache.primary_keys("posts") end @@ -75,8 +75,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) assert_no_queries do - assert_equal 11, @cache.columns("posts").size - assert_equal 11, @cache.columns_hash("posts").size + assert_equal 12, @cache.columns("posts").size + assert_equal 12, @cache.columns_hash("posts").size assert @cache.data_sources("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 index 917a04ebc3..1c79d776f0 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -82,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_bigint_limit - cast_type = @connection.send(:type_map).lookup("bigint") + limit = @connection.send(:type_map).lookup("bigint").send(:_limit) if current_adapter?(:OracleAdapter) - assert_equal 19, cast_type.limit + assert_equal 19, limit else - assert_equal 8, cast_type.limit + assert_equal 8, limit end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 9d6ecbde78..0941ee3309 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -27,7 +27,7 @@ module ActiveRecord # make sure we have an active connection assert ActiveRecord::Base.connection - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_app_delegation @@ -47,14 +47,14 @@ module ActiveRecord def test_connections_are_cleared_after_body_close _, _, body = @management.call(@env) body.close - assert !ActiveRecord::Base.connection_handler.active_connections? + assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_active_connections_are_not_cleared_on_body_close_during_transaction ActiveRecord::Base.transaction do _, _, body = @management.call(@env) body.close - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end @@ -62,7 +62,7 @@ module ActiveRecord app = Class.new(App) { def call(env); raise NotImplementedError; end }.new explosive = middleware(app) assert_raises(NotImplementedError) { explosive.call(@env) } - assert !ActiveRecord::Base.connection_handler.active_connections? + assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_connections_not_closed_if_exception_inside_transaction @@ -70,14 +70,14 @@ module ActiveRecord app = Class.new(App) { def call(env); raise RuntimeError; end }.new explosive = middleware(app) assert_raises(RuntimeError) { explosive.call(@env) } - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end test "doesn't clear active connections when running in a test case" do executor.wrap do @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end @@ -85,7 +85,7 @@ module ActiveRecord body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } response_body = middleware(app).call(@env)[2] - assert response_body.respond_to?(:to_path) + assert_respond_to response_body, :to_path assert_equal "/path", response_body.to_path end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index cb29c578b7..9ac03629c3 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -35,12 +35,12 @@ module ActiveRecord def test_checkout_after_close connection = pool.connection - assert connection.in_use? + assert_predicate connection, :in_use? connection.close - assert !connection.in_use? + assert_not_predicate connection, :in_use? - assert pool.connection.in_use? + assert_predicate pool.connection, :in_use? end def test_released_connection_moves_between_threads @@ -80,14 +80,14 @@ module ActiveRecord end def test_active_connection_in_use - assert !pool.active_connection? + assert_not_predicate pool, :active_connection? main_thread = pool.connection - assert pool.active_connection? + assert_predicate pool, :active_connection? main_thread.close - assert !pool.active_connection? + assert_not_predicate pool, :active_connection? end def test_full_pool_exception @@ -205,11 +205,11 @@ module ActiveRecord def test_remove_connection conn = @pool.checkout - assert conn.in_use? + assert_predicate conn, :in_use? length = @pool.connections.length @pool.remove conn - assert conn.in_use? + assert_predicate conn, :in_use? assert_equal(length - 1, @pool.connections.length) ensure conn.close @@ -224,11 +224,11 @@ module ActiveRecord end def test_active_connection? - assert !@pool.active_connection? + assert_not_predicate @pool, :active_connection? assert @pool.connection - assert @pool.active_connection? + assert_predicate @pool, :active_connection? @pool.release_connection - assert !@pool.active_connection? + assert_not_predicate @pool, :active_connection? end def test_checkout_behaviour @@ -469,7 +469,7 @@ module ActiveRecord end def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns - Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout [:disconnect, :clear_reloadable_connections].each do |group_action_method| @pool.with_connection do |connection| @@ -479,7 +479,7 @@ module ActiveRecord end end ensure - Thread.report_on_exception = original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = original_report_on_exception end def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns @@ -496,7 +496,7 @@ module ActiveRecord assert_nil timed_join_result # assert that since this is within default timeout our connection hasn't been forcefully taken away from us - assert @pool.active_connection? + assert_predicate @pool, :active_connection? end ensure thread.join if thread && !timed_join_result # clean up the other thread @@ -510,7 +510,7 @@ module ActiveRecord @pool.with_connection do |connection| Thread.new { @pool.send(group_action_method) }.join # assert connection has been forcefully taken away from us - assert_not @pool.active_connection? + assert_not_predicate @pool, :active_connection? # make a new connection for with_connection to clean up @pool.connection diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 356afdbd2b..6e7ae2efb4 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -4,7 +4,6 @@ require "cases/helper" require "models/person" require "models/topic" require "pp" -require "active_support/core_ext/string/strip" class NonExistentTable < ActiveRecord::Base; end @@ -39,26 +38,26 @@ class CoreTest < ActiveRecord::TestCase topic = Topic.new actual = "".dup 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> + expected = <<~PRETTY + #<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) @@ -68,26 +67,26 @@ class CoreTest < ActiveRecord::TestCase topic = topics(:first) actual = "".dup 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: [^,>]+> + expected = <<~PRETTY + #<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 diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index e0948f90ac..99d286dc52 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -280,38 +280,38 @@ class CounterCacheTest < ActiveRecord::TestCase end test "update counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.update_counters(@topic.id, replies_count: -1, touch: :written_on) end end test "update multiple counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: :written_on) end end test "reset counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.reset_counters(@topic.id, :replies, touch: :written_on) end end test "reset multiple counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: :written_on) end end test "increment counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.increment_counter(:replies_count, @topic.id, touch: :written_on) end end test "decrement counters with touch: :written_on" do - assert_touching @topic, :written_on do + assert_touching @topic, :updated_at, :written_on do Topic.decrement_counter(:replies_count, @topic.id, touch: :written_on) end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 51f6164138..e64a8372d0 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 5, Foo.columns_hash["updated_at"].precision end + def test_datetime_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :created_at, :datetime, precision: 0 + @connection.add_column :foos, :updated_at, :datetime, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(created_at: time, updated_at: time) + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + end + def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 3d11b573f1..0f957d41cf 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -9,7 +9,7 @@ class DefaultTest < ActiveRecord::TestCase def test_nil_defaults_for_not_null_columns %w(id name course_id).each do |name| column = Entrant.columns_hash[name] - assert !column.null, "#{name} column should be NOT NULL" + assert_not column.null, "#{name} column should be NOT NULL" assert_not column.default, "#{name} column should be DEFAULT 'nil'" end end @@ -106,21 +106,31 @@ if current_adapter?(:Mysql2Adapter) class MysqlDefaultExpressionTest < ActiveRecord::TestCase include SchemaDumpingHelper - if ActiveRecord::Base.connection.version >= "5.6.0" + if subsecond_precision_supported? test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") - assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output end - end - test "schema dump timestamp includes default expression" do - output = dump_table_schema("timestamp_defaults") - assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output - end + test "schema dump datetime includes precise default expression" do + output = dump_table_schema("datetime_defaults") + assert_match %r/t\.datetime\s+"precise_datetime",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output + end - test "schema dump timestamp without default expression" do - output = dump_table_schema("timestamp_defaults") - assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + test "schema dump timestamp includes default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output + end + + test "schema dump timestamp includes precise default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"precise_timestamp",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output + end + + test "schema dump timestamp without default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index d4408776d3..83cc2aa319 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -24,18 +24,18 @@ class DirtyTest < ActiveRecord::TestCase # Change catchphrase. pirate.catchphrase = "arrr" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_was assert_equal [nil, "arrr"], pirate.catchphrase_change # Saved - no changes. pirate.save! - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_change # Same value - no changes. pirate.catchphrase = "arrr" - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_change end @@ -46,23 +46,23 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now - 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was assert_equal old_created_on, pirate.created_on_was pirate.created_on = old_created_on - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end end @@ -73,7 +73,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = target.create! pirate.created_on = pirate.created_on - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end end @@ -86,19 +86,19 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? # kind_of does not work because # ActiveSupport::TimeWithZone.name == 'Time' assert_instance_of Time, pirate.created_on_was @@ -113,19 +113,19 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? # kind_of does not work because # ActiveSupport::TimeWithZone.name == 'Time' assert_instance_of Time, pirate.created_on_was @@ -137,11 +137,11 @@ class DirtyTest < ActiveRecord::TestCase # the actual attribute here is name, title is an # alias setup via alias_attribute parrot = Parrot.new - assert !parrot.title_changed? + assert_not_predicate parrot, :title_changed? assert_nil parrot.title_change parrot.name = "Sam" - assert parrot.title_changed? + assert_predicate parrot, :title_changed? assert_nil parrot.title_was assert_equal parrot.name_change, parrot.title_change end @@ -153,7 +153,7 @@ class DirtyTest < ActiveRecord::TestCase pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? end def test_nullable_number_not_marked_as_changed_if_new_value_is_blank @@ -161,7 +161,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| pirate.parrot_id = value - assert !pirate.parrot_id_changed? + assert_not_predicate pirate, :parrot_id_changed? assert_nil pirate.parrot_id_change end end @@ -171,7 +171,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| numeric_data.bank_balance = value - assert !numeric_data.bank_balance_changed? + assert_not_predicate numeric_data, :bank_balance_changed? assert_nil numeric_data.bank_balance_change end end @@ -181,7 +181,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| numeric_data.temperature = value - assert !numeric_data.temperature_changed? + assert_not_predicate numeric_data, :temperature_changed? assert_nil numeric_data.temperature_change end end @@ -197,7 +197,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| topic.written_on = value assert_nil topic.written_on - assert !topic.written_on_changed? + assert_not_predicate topic, :written_on_changed? end end end @@ -208,10 +208,10 @@ class DirtyTest < ActiveRecord::TestCase pirate.catchphrase = "arrr" assert pirate.save! - assert !pirate.changed? + assert_not_predicate pirate, :changed? pirate.parrot_id = "0" - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_integer_zero_to_integer_zero_not_marked_as_changed @@ -220,17 +220,17 @@ class DirtyTest < ActiveRecord::TestCase pirate.catchphrase = "arrr" assert pirate.save! - assert !pirate.changed? + assert_not_predicate pirate, :changed? pirate.parrot_id = 0 - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_float_zero_to_string_zero_not_marked_as_changed data = NumericData.new temperature: 0.0 data.save! - assert_not data.changed? + assert_not_predicate data, :changed? data.temperature = "0" assert_empty data.changes @@ -251,38 +251,38 @@ class DirtyTest < ActiveRecord::TestCase # check the change from 1 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = "" - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([1, nil], pirate.parrot_id_change) pirate.save # check the change from nil to 0 pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = 0 - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([nil, 0], pirate.parrot_id_change) pirate.save # check the change from 0 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = "" - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([0, nil], pirate.parrot_id_change) end def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new - assert !pirate.changed? + assert_not_predicate pirate, :changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes pirate.catchphrase = "arrr" - assert pirate.changed? + assert_predicate pirate, :changed? assert_nil pirate.catchphrase_was assert_equal %w(catchphrase), pirate.changed assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes) pirate.save - assert !pirate.changed? + assert_not_predicate pirate, :changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes end @@ -290,40 +290,40 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(catchphrase: "arr") - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert pirate.catchphrase_will_change! - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_equal ["arr", "arr"], pirate.catchphrase_change pirate.catchphrase << " matey!" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_equal ["arr", "arr matey!"], pirate.catchphrase_change end def test_virtual_attribute_will_change parrot = Parrot.create!(name: "Ruby") parrot.send(:attribute_will_change!, :cancel_save_from_callback) - assert parrot.has_changes_to_save? + assert_predicate parrot, :has_changes_to_save? end def test_association_assignment_changes_foreign_key pirate = Pirate.create!(catchphrase: "jarl") pirate.parrot = Parrot.create!(name: "Lorre") - assert pirate.changed? + assert_predicate pirate, :changed? assert_equal %w(parrot_id), pirate.changed end def test_attribute_should_be_compared_with_type_cast topic = Topic.new - assert topic.approved? - assert !topic.approved_changed? + assert_predicate topic, :approved? + assert_not_predicate topic, :approved_changed? # Coming from web form. params = { topic: { approved: 1 } } # In the controller. topic.attributes = params[:topic] - assert topic.approved? - assert !topic.approved_changed? + assert_predicate topic, :approved? + assert_not_predicate topic, :approved_changed? end def test_partial_update @@ -366,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase def test_changed_attributes_should_be_preserved_if_save_failure pirate = Pirate.new pirate.parrot_id = 1 - assert !pirate.save + assert_not pirate.save check_pirate_after_save_failure(pirate) pirate = Pirate.new @@ -378,9 +378,9 @@ class DirtyTest < ActiveRecord::TestCase def test_reload_should_clear_changed_attributes pirate = Pirate.create!(catchphrase: "shiver me timbers") pirate.catchphrase = "*hic*" - assert pirate.changed? + assert_predicate pirate, :changed? pirate.reload - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_dup_objects_should_not_copy_dirty_flag_from_creator @@ -388,17 +388,17 @@ class DirtyTest < ActiveRecord::TestCase pirate_dup = pirate.dup pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" - assert pirate.catchphrase_changed? - assert !pirate_dup.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? + assert_not_predicate pirate_dup, :catchphrase_changed? end def test_reverted_changes_are_not_dirty phrase = "shiver me timbers" pirate = Pirate.create!(catchphrase: phrase) pirate.catchphrase = "*hic*" - assert pirate.changed? + assert_predicate pirate, :changed? pirate.catchphrase = phrase - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_reverted_changes_are_not_dirty_after_multiple_changes @@ -406,40 +406,40 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: phrase) 10.times do |i| pirate.catchphrase = "*hic*" * i - assert pirate.changed? + assert_predicate pirate, :changed? end - assert pirate.changed? + assert_predicate pirate, :changed? pirate.catchphrase = phrase - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back pirate = Pirate.create!(catchphrase: "Yar!") pirate.parrot_id = 1 - assert pirate.changed? - assert pirate.parrot_id_changed? - assert !pirate.catchphrase_changed? + assert_predicate pirate, :changed? + assert_predicate pirate, :parrot_id_changed? + assert_not_predicate pirate, :catchphrase_changed? pirate.parrot_id = nil - assert !pirate.changed? - assert !pirate.parrot_id_changed? - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :changed? + assert_not_predicate pirate, :parrot_id_changed? + assert_not_predicate pirate, :catchphrase_changed? end 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? + assert_not_predicate topic, :changed? topic.content[:b] = "b" - assert topic.changed? + assert_predicate topic, :changed? topic.save! - assert_not topic.changed? + assert_not_predicate topic, :changed? assert_equal "b", topic.content[:b] topic.reload @@ -473,6 +473,14 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_changes_to_save_should_not_mutate_array_of_hashes + topic = Topic.new(author_name: "Bill", content: [{ a: "a" }]) + + topic.changes_to_save + + assert_equal [{ a: "a" }], topic.content + end + def test_previous_changes # original values should be in previous_changes pirate = Pirate.new @@ -488,7 +496,7 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_nil pirate.previous_changes["created_on"][0] assert_not_nil pirate.previous_changes["created_on"][1] - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") # original values should be in previous_changes pirate = Pirate.new @@ -502,7 +510,7 @@ class DirtyTest < ActiveRecord::TestCase assert_equal [nil, pirate.id], pirate.previous_changes["id"] assert_includes pirate.previous_changes, "updated_on" assert_includes pirate.previous_changes, "created_on" - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") pirate.catchphrase = "Yar!!" pirate.reload @@ -519,8 +527,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") pirate = Pirate.find_by_catchphrase("Me Maties!") @@ -533,8 +541,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -545,8 +553,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -557,8 +565,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") ensure travel_back end @@ -596,7 +604,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: "rrrr", created_on: time_in_paris) pirate.created_on = pirate.created_on.in_time_zone("Tokyo").to_s - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end test "partial insert" do @@ -627,7 +635,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", "arrrr matey!"] } @@ -641,7 +649,7 @@ class DirtyTest < ActiveRecord::TestCase pirate.reload assert_equal "arrrr matey!", pirate.catchphrase - assert_not pirate.changed? + assert_not_predicate pirate, :changed? end test "in place mutation for binary" do @@ -652,19 +660,19 @@ class DirtyTest < ActiveRecord::TestCase binary = klass.create!(data: "\\\\foo") - assert_not binary.changed? + assert_not_predicate binary, :changed? binary.data = binary.data.dup - assert_not binary.changed? + assert_not_predicate binary, :changed? binary = klass.last - assert_not binary.changed? + assert_not_predicate binary, :changed? binary.data << "bar" - assert binary.changed? + assert_predicate binary, :changed? end test "changes is correct for subclass" do @@ -679,7 +687,7 @@ class DirtyTest < ActiveRecord::TestCase new_catchphrase = "arrrr matey!" pirate.catchphrase = new_catchphrase - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", new_catchphrase] @@ -698,7 +706,7 @@ class DirtyTest < ActiveRecord::TestCase new_catchphrase = "arrrr matey!" pirate.catchphrase = new_catchphrase - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", new_catchphrase] @@ -720,7 +728,7 @@ class DirtyTest < ActiveRecord::TestCase end model = klass.new(first_name: "Jim") - assert model.first_name_changed? + assert_predicate model, :first_name_changed? end test "attribute_will_change! doesn't try to save non-persistable attributes" do @@ -732,10 +740,28 @@ class DirtyTest < ActiveRecord::TestCase record = klass.new(first_name: "Sean") record.non_persisted_attribute_will_change! - assert record.non_persisted_attribute_changed? + assert_predicate record, :non_persisted_attribute_changed? assert record.save end + test "virtual attributes are not written with partial_writes off" do + with_partial_writes(ActiveRecord::Base, false) do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + attribute :non_persisted_attribute, :string + end + + record = klass.new(first_name: "Sean") + record.non_persisted_attribute_will_change! + + assert record.save + + record.non_persisted_attribute_will_change! + + assert record.save + end + end + test "mutating and then assigning doesn't remove the change" do pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" @@ -762,13 +788,13 @@ class DirtyTest < ActiveRecord::TestCase test "attributes assigned but not selected are dirty" do person = Person.select(:id).first - refute person.changed? + assert_not_predicate person, :changed? person.first_name = "Sean" - assert person.changed? + assert_predicate person, :changed? person.first_name = nil - assert person.changed? + assert_predicate person, :changed? end test "attributes not selected are still missing after save" do @@ -781,14 +807,14 @@ class DirtyTest < ActiveRecord::TestCase test "saved_change_to_attribute? returns whether a change occurred in the last save" do person = Person.create!(first_name: "Sean") - assert person.saved_change_to_first_name? - refute person.saved_change_to_gender? + assert_predicate person, :saved_change_to_first_name? + assert_not_predicate person, :saved_change_to_gender? assert person.saved_change_to_first_name?(from: nil, to: "Sean") assert person.saved_change_to_first_name?(from: nil) assert person.saved_change_to_first_name?(to: "Sean") - refute person.saved_change_to_first_name?(from: "Jim", to: "Sean") - refute person.saved_change_to_first_name?(from: "Jim") - refute person.saved_change_to_first_name?(to: "Jim") + assert_not person.saved_change_to_first_name?(from: "Jim", to: "Sean") + assert_not person.saved_change_to_first_name?(from: "Jim") + assert_not person.saved_change_to_first_name?(to: "Jim") end test "saved_change_to_attribute returns the change that occurred in the last save" do @@ -823,11 +849,11 @@ class DirtyTest < ActiveRecord::TestCase test "saved_changes? returns whether the last call to save changed anything" do person = Person.create!(first_name: "Sean") - assert person.saved_changes? + assert_predicate person, :saved_changes? person.save - refute person.saved_changes? + assert_not_predicate person, :saved_changes? end test "saved_changes returns a hash of all the changes that occurred" do @@ -857,7 +883,7 @@ class DirtyTest < ActiveRecord::TestCase end person = klass.create!(first_name: "Sean") - refute person.changed? + assert_not_predicate person, :changed? end private @@ -870,8 +896,8 @@ class DirtyTest < ActiveRecord::TestCase end def check_pirate_after_save_failure(pirate) - assert pirate.changed? - assert pirate.parrot_id_changed? + assert_predicate pirate, :changed? + assert_predicate pirate, :parrot_id_changed? assert_equal %w(parrot_id), pirate.changed assert_nil pirate.parrot_id_was end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 73da31996e..a2efbf89f9 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -3,20 +3,21 @@ require "cases/helper" require "models/reply" require "models/topic" +require "models/movie" module ActiveRecord class DupTest < ActiveRecord::TestCase fixtures :topics def test_dup - assert !Topic.new.freeze.dup.frozen? + assert_not_predicate Topic.new.freeze.dup, :frozen? end def test_not_readonly topic = Topic.first duped = topic.dup - assert !duped.readonly?, "should not be readonly" + assert_not duped.readonly?, "should not be readonly" end def test_is_readonly @@ -31,7 +32,7 @@ module ActiveRecord topic = Topic.first duped = topic.dup - assert !duped.persisted?, "topic not persisted" + assert_not duped.persisted?, "topic not persisted" assert duped.new_record?, "topic is new" end @@ -40,7 +41,7 @@ module ActiveRecord topic.destroy duped = topic.dup - assert_not duped.destroyed? + assert_not_predicate duped, :destroyed? end def test_dup_has_no_id @@ -126,12 +127,12 @@ module ActiveRecord duped = topic.dup duped.title = nil - assert duped.invalid? + assert_predicate duped, :invalid? topic.title = nil duped.title = "Mathematics" - assert topic.invalid? - assert duped.valid? + assert_predicate topic, :invalid? + assert_predicate duped, :valid? end end @@ -139,7 +140,7 @@ module ActiveRecord prev_default_scopes = Topic.default_scopes Topic.default_scopes = [proc { Topic.where(approved: true) }] topic = Topic.new(approved: false) - assert !topic.dup.approved?, "should not be overridden by default scopes" + assert_not topic.dup.approved?, "should not be overridden by default scopes" ensure Topic.default_scopes = prev_default_scopes end @@ -157,5 +158,20 @@ module ActiveRecord record.dup end end + + def test_dup_record_not_persisted_after_rollback_transaction + movie = Movie.new(name: "test") + + assert_raises(ActiveRecord::RecordInvalid) do + Movie.transaction do + movie.save! + duped = movie.dup + duped.name = nil + duped.save! + end + end + + assert_not movie.persisted? + end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 7cda712112..d5a1d11e12 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -12,16 +12,16 @@ class EnumTest < ActiveRecord::TestCase end test "query state by predicate" do - assert @book.published? - assert_not @book.written? - assert_not @book.proposed? + assert_predicate @book, :published? + assert_not_predicate @book, :written? + assert_not_predicate @book, :proposed? - assert @book.read? - assert @book.in_english? - assert @book.author_visibility_visible? - assert @book.illustrator_visibility_visible? - assert @book.with_medium_font_size? - assert @book.medium_to_read? + assert_predicate @book, :read? + assert_predicate @book, :in_english? + assert_predicate @book, :author_visibility_visible? + assert_predicate @book, :illustrator_visibility_visible? + assert_predicate @book, :with_medium_font_size? + assert_predicate @book, :medium_to_read? end test "query state with strings" do @@ -76,46 +76,46 @@ class EnumTest < ActiveRecord::TestCase end test "build from scope" do - assert Book.written.build.written? - assert_not Book.written.build.proposed? + assert_predicate Book.written.build, :written? + assert_not_predicate Book.written.build, :proposed? end test "build from where" do - assert Book.where(status: Book.statuses[:written]).build.written? - assert_not Book.where(status: Book.statuses[:written]).build.proposed? - assert Book.where(status: :written).build.written? - assert_not Book.where(status: :written).build.proposed? - assert Book.where(status: "written").build.written? - assert_not Book.where(status: "written").build.proposed? + assert_predicate Book.where(status: Book.statuses[:written]).build, :written? + assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed? + assert_predicate Book.where(status: :written).build, :written? + assert_not_predicate Book.where(status: :written).build, :proposed? + assert_predicate Book.where(status: "written").build, :written? + assert_not_predicate Book.where(status: "written").build, :proposed? end test "update by declaration" do @book.written! - assert @book.written? + assert_predicate @book, :written? @book.in_english! - assert @book.in_english? + assert_predicate @book, :in_english? @book.author_visibility_visible! - assert @book.author_visibility_visible? + assert_predicate @book, :author_visibility_visible? end test "update by setter" do @book.update! status: :written - assert @book.written? + assert_predicate @book, :written? end test "enum methods are overwritable" do assert_equal "do publish work...", @book.published! - assert @book.published? + assert_predicate @book, :published? end test "direct assignment" do @book.status = :written - assert @book.written? + assert_predicate @book, :written? end test "assign string value" do @book.status = "written" - assert @book.written? + assert_predicate @book, :written? end test "enum changed attributes" do @@ -242,17 +242,17 @@ class EnumTest < ActiveRecord::TestCase end test "building new objects with enum scopes" do - assert Book.written.build.written? - assert Book.read.build.read? - assert Book.in_spanish.build.in_spanish? - assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible? + assert_predicate Book.written.build, :written? + assert_predicate Book.read.build, :read? + assert_predicate Book.in_spanish.build, :in_spanish? + assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible? end test "creating new objects with enum scopes" do - assert Book.written.create.written? - assert Book.read.create.read? - assert Book.in_spanish.create.in_spanish? - assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? + assert_predicate Book.written.create, :written? + assert_predicate Book.read.create, :read? + assert_predicate Book.in_spanish.create, :in_spanish? + assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible? end test "_before_type_cast" do @@ -355,9 +355,9 @@ class EnumTest < ActiveRecord::TestCase klass.delete_all klass.create!(status: "proposed") book = klass.new(status: "written") - assert book.valid? + assert_predicate book, :valid? book.status = "proposed" - assert_not book.valid? + assert_not_predicate book, :valid? end test "validate inclusion of value in array" do @@ -368,9 +368,9 @@ class EnumTest < ActiveRecord::TestCase end klass.delete_all invalid_book = klass.new(status: "proposed") - assert_not invalid_book.valid? + assert_not_predicate invalid_book, :valid? valid_book = klass.new(status: "written") - assert valid_book.valid? + assert_predicate valid_book, :valid? end test "enums are distinct per class" do @@ -417,10 +417,10 @@ class EnumTest < ActiveRecord::TestCase end book1 = klass.proposed.create! - assert book1.proposed? + assert_predicate book1, :proposed? book2 = klass.single.create! - assert book2.single? + assert_predicate book2, :single? end test "enum with alias_attribute" do @@ -431,62 +431,62 @@ class EnumTest < ActiveRecord::TestCase end book = klass.proposed.create! - assert book.proposed? + assert_predicate book, :proposed? assert_equal "proposed", book.aliased_status book = klass.find(book.id) - assert book.proposed? + assert_predicate book, :proposed? assert_equal "proposed", book.aliased_status end test "query state by predicate with prefix" do - assert @book.author_visibility_visible? - assert_not @book.author_visibility_invisible? - assert @book.illustrator_visibility_visible? - assert_not @book.illustrator_visibility_invisible? + assert_predicate @book, :author_visibility_visible? + assert_not_predicate @book, :author_visibility_invisible? + assert_predicate @book, :illustrator_visibility_visible? + assert_not_predicate @book, :illustrator_visibility_invisible? end test "query state by predicate with custom prefix" do - assert @book.in_english? - assert_not @book.in_spanish? - assert_not @book.in_french? + assert_predicate @book, :in_english? + assert_not_predicate @book, :in_spanish? + assert_not_predicate @book, :in_french? end test "query state by predicate with custom suffix" do - assert @book.medium_to_read? - assert_not @book.easy_to_read? - assert_not @book.hard_to_read? + assert_predicate @book, :medium_to_read? + assert_not_predicate @book, :easy_to_read? + assert_not_predicate @book, :hard_to_read? end test "enum methods with custom suffix defined" do - assert @book.class.respond_to?(:easy_to_read) - assert @book.class.respond_to?(:medium_to_read) - assert @book.class.respond_to?(:hard_to_read) + assert_respond_to @book.class, :easy_to_read + assert_respond_to @book.class, :medium_to_read + assert_respond_to @book.class, :hard_to_read - assert @book.respond_to?(:easy_to_read?) - assert @book.respond_to?(:medium_to_read?) - assert @book.respond_to?(:hard_to_read?) + assert_respond_to @book, :easy_to_read? + assert_respond_to @book, :medium_to_read? + assert_respond_to @book, :hard_to_read? - assert @book.respond_to?(:easy_to_read!) - assert @book.respond_to?(:medium_to_read!) - assert @book.respond_to?(:hard_to_read!) + assert_respond_to @book, :easy_to_read! + assert_respond_to @book, :medium_to_read! + assert_respond_to @book, :hard_to_read! end test "update enum attributes with custom suffix" do @book.medium_to_read! - assert_not @book.easy_to_read? - assert @book.medium_to_read? - assert_not @book.hard_to_read? + assert_not_predicate @book, :easy_to_read? + assert_predicate @book, :medium_to_read? + assert_not_predicate @book, :hard_to_read? @book.easy_to_read! - assert @book.easy_to_read? - assert_not @book.medium_to_read? - assert_not @book.hard_to_read? + assert_predicate @book, :easy_to_read? + assert_not_predicate @book, :medium_to_read? + assert_not_predicate @book, :hard_to_read? @book.hard_to_read! - assert_not @book.easy_to_read? - assert_not @book.medium_to_read? - assert @book.hard_to_read? + assert_not_predicate @book, :easy_to_read? + assert_not_predicate @book, :medium_to_read? + assert_predicate @book, :hard_to_read? end test "uses default status when no status is provided in fixtures" do @@ -497,12 +497,12 @@ class EnumTest < ActiveRecord::TestCase test "uses default value from database on initialization" do book = Book.new - assert book.proposed? + assert_predicate book, :proposed? end test "uses default value from database on initialization when using custom mapping" do book = Book.new - assert book.hard? + assert_predicate book, :hard? end test "data type of Enum type" do diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index fb698c47cd..82cc891970 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -15,20 +15,20 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_the_payload_has_an_exception SUBSCRIBER.finish(nil, nil, exception: Exception.new) - assert queries.empty? + assert_empty queries end def test_collects_nothing_for_ignored_payloads ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| SUBSCRIBER.finish(nil, nil, name: ip) end - assert queries.empty? + assert_empty queries end def test_collects_nothing_if_collect_is_false ActiveRecord::ExplainRegistry.collect = false SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select 1 from users", binds: [1, 2]) - assert queries.empty? + assert_empty queries end def test_collects_pairs_of_queries_and_binds @@ -42,12 +42,12 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_the_statement_is_not_whitelisted SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") - assert queries.empty? + assert_empty queries end def test_collects_nothing_if_the_statement_is_only_partially_matched SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select_db yo_mama") - assert queries.empty? + assert_empty queries end def test_collects_cte_queries diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 17654027a9..a0e75f4e89 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require "models/car" -require "active_support/core_ext/string/strip" if ActiveRecord::Base.connection.supports_explain? class ExplainTest < ActiveRecord::TestCase @@ -53,7 +52,7 @@ if ActiveRecord::Base.connection.supports_explain? queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do - expected = <<-SQL.strip_heredoc + expected = <<~SQL EXPLAIN for: #{sqls[0]} [["wadus", 1]] query plan foo diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 4039af66d0..59af4e6961 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -8,7 +8,7 @@ class FinderRespondToTest < ActiveRecord::TestCase def test_should_preserve_normal_respond_to_behaviour_on_base assert_respond_to ActiveRecord::Base, :new - assert !ActiveRecord::Base.respond_to?(:find_by_something) + assert_not_respond_to ActiveRecord::Base, :find_by_something end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method @@ -43,14 +43,14 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_not_respond_to_find_by_one_missing_attribute - assert !Topic.respond_to?(:find_by_undertitle) + assert_not_respond_to Topic, :find_by_undertitle end def test_should_not_respond_to_find_by_invalid_method_syntax - assert !Topic.respond_to?(:fail_to_find_by_title) - assert !Topic.respond_to?(:find_by_title?) - assert !Topic.respond_to?(:fail_to_find_or_create_by_title) - assert !Topic.respond_to?(:find_or_create_by_title?) + assert_not_respond_to Topic, :fail_to_find_by_title + assert_not_respond_to Topic, :find_by_title? + assert_not_respond_to Topic, :fail_to_find_or_create_by_title + assert_not_respond_to Topic, :find_or_create_by_title? end private diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 8369a10b5a..e73c88dd5d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -270,25 +270,27 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_with_includes_limit_and_empty_result - assert_equal false, Topic.includes(:replies).limit(0).exists? - assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? + assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? } + assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? } end def test_exists_with_distinct_association_includes_and_limit author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments) + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end 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.tags_count DESC").limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC") + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association - developer = developers(:david) - assert_not_predicate developer.ratings.includes(comment: :post).where(posts: { id: 1 }), :exists? + ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 }) + assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? } end def test_exists_with_empty_table_and_no_args_given @@ -353,6 +355,12 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_relation_with_large_number + assert_raises(ActiveRecord::RecordNotFound) do + Topic.where("1=1").find(9999999999999999999999999999999) + end + end + + def test_find_by_on_relation_with_large_number assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999) end @@ -412,7 +420,7 @@ class FinderTest < ActiveRecord::TestCase end def test_take - assert_equal topics(:first), Topic.take + assert_equal topics(:first), Topic.where("title = 'The First Topic'").take end def test_take_failing @@ -455,6 +463,7 @@ class FinderTest < ActiveRecord::TestCase expected = topics(:first) expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.first + assert_equal expected, Topic.limit(5).first end def test_model_class_responds_to_first_bang @@ -477,6 +486,7 @@ class FinderTest < ActiveRecord::TestCase expected = topics(:second) expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.second + assert_equal expected, Topic.limit(5).second end def test_model_class_responds_to_second_bang @@ -499,6 +509,7 @@ class FinderTest < ActiveRecord::TestCase expected = topics(:third) expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.third + assert_equal expected, Topic.limit(5).third end def test_model_class_responds_to_third_bang @@ -521,6 +532,7 @@ class FinderTest < ActiveRecord::TestCase expected = topics(:fourth) expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.fourth + assert_equal expected, Topic.limit(5).fourth end def test_model_class_responds_to_fourth_bang @@ -543,6 +555,7 @@ class FinderTest < ActiveRecord::TestCase expected = topics(:fifth) expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.fifth + assert_equal expected, Topic.limit(5).fifth end def test_model_class_responds_to_fifth_bang @@ -646,13 +659,13 @@ class FinderTest < ActiveRecord::TestCase def test_last_with_integer_and_order_should_use_sql_limit relation = Topic.order("title") assert_queries(1) { relation.last(5) } - assert !relation.loaded? + assert_not_predicate relation, :loaded? end def test_last_with_integer_and_reorder_should_use_sql_limit relation = Topic.reorder("title") assert_queries(1) { relation.last(5) } - assert !relation.loaded? + assert_not_predicate relation, :loaded? end def test_last_on_loaded_relation_should_not_use_sql @@ -677,12 +690,42 @@ class FinderTest < ActiveRecord::TestCase assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + assert_equal comments.offset(2).to_a.last, comments.offset(2).last + assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2) + assert_equal comments.offset(2).to_a.last(3), comments.offset(2).last(3) + comments = comments.offset(1) assert_equal comments.limit(2).to_a.last, comments.limit(2).last assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end + def test_first_on_relation_with_limit_and_offset + post = posts("sti_comments") + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + + assert_equal comments.offset(2).to_a.first, comments.offset(2).first + assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2) + assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + end + + def test_first_have_determined_order_by_default + expected = [companies(:second_client), companies(:another_client)] + clients = Client.where(name: expected.map(&:name)) + + assert_equal expected, clients.first(2) + assert_equal expected, clients.limit(5).first(2) + end + def test_take_and_first_and_last_with_integer_should_return_an_array assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) @@ -703,8 +746,8 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveModel::MissingAttributeError) { topic.title? } assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name - assert !topic.attribute_present?("title") - assert !topic.attribute_present?(:title) + assert_not topic.attribute_present?("title") + assert_not topic.attribute_present?(:title) assert topic.attribute_present?("author_name") assert_respond_to topic, "author_name" end @@ -788,6 +831,15 @@ class FinderTest < ActiveRecord::TestCase assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort end + def test_find_on_hash_conditions_with_open_ended_range + assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort + end + + def test_find_on_hash_conditions_with_numeric_range_for_string + topic = Topic.create!(title: "12 Factor App") + assert_equal [topic], Topic.where(title: 10..2).to_a + end + def test_find_on_multiple_hash_conditions assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } @@ -844,6 +896,25 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_hash_condition_find_with_aggregate_having_three_mappings_array + david_address = customers(:david).address + zaphod_address = customers(:zaphod).address + barney_address = customers(:barney).address + assert_kind_of Address, david_address + assert_kind_of Address, zaphod_address + found_customers = Customer.where(address: [david_address, zaphod_address, barney_address]) + assert_equal [customers(:david), customers(:zaphod), customers(:barney)], found_customers.sort_by(&:id) + end + + def test_hash_condition_find_with_aggregate_having_one_mapping_array + david_balance = customers(:david).balance + zaphod_balance = customers(:zaphod).balance + assert_kind_of Money, david_balance + assert_kind_of Money, zaphod_balance + found_customers = Customer.where(balance: [david_balance, zaphod_balance]) + assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id) + end + def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location @@ -1049,14 +1120,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") } end - def test_find_last_with_offset - devs = Developer.order("id") - - assert_equal devs[2], Developer.offset(2).first - assert_equal devs[-3], Developer.offset(2).last - assert_equal devs[-3], Developer.offset(2).order("id DESC").first - end - def test_find_by_nil_attribute topic = Topic.find_by_last_read nil assert_not_nil topic @@ -1145,6 +1208,11 @@ class FinderTest < ActiveRecord::TestCase order("author_addresses_authors.id DESC").limit(3).to_a.size end + def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key + assert_equal Post.first, Post.eager_load(comments: :ratings). + order("posts.id, ratings.id, comments.id").first + end + def test_find_with_nil_inside_set_passed_for_one_attribute client_of = Company. where(client_of: [2, 1, nil], @@ -1301,12 +1369,12 @@ class FinderTest < ActiveRecord::TestCase test "#skip_query_cache! for #exists? with a limited eager load" do Topic.cache do - assert_queries(2) do + assert_queries(1) do Topic.eager_load(:replies).limit(1).exists? Topic.eager_load(:replies).limit(1).exists? end - assert_queries(4) do + assert_queries(2) do Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8e8a49af8e..c65523d8c1 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "cases/helper" +require "support/connection_helper" require "models/admin" require "models/admin/account" require "models/admin/randomly_named_c1" @@ -32,6 +33,8 @@ require "models/treasure" require "tempfile" class FixturesTest < ActiveRecord::TestCase + include ConnectionHelper + self.use_instantiated_fixtures = true self.use_transactional_tests = false @@ -79,6 +82,239 @@ class FixturesTest < ActiveRecord::TestCase ActiveSupport::Notifications.unsubscribe(subscription) end end + + def test_bulk_insert_multiple_table_with_a_multi_statement_query + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + + create_fixtures("bulbs", "authors", "computers") + + expected_sql = <<~EOS.chop + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .* + EOS + assert_equal 1, subscriber.events.size + assert_match(/#{expected_sql}/, subscriber.events.first) + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails + require "models/aircraft" + + assert_equal false, Aircraft.columns_hash["wheels_count"].null + fixtures = { + "aircraft" => [ + { "name" => "working_aircrafts", "wheels_count" => 2 }, + { "name" => "broken_aircrafts", "wheels_count" => nil }, + ] + } + + assert_no_difference "Aircraft.count" do + assert_raises(ActiveRecord::NotNullViolation) do + ActiveRecord::Base.connection.insert_fixtures_set(fixtures) + end + end + end + + def test_bulk_insert_with_a_multi_statement_query_in_a_nested_transaction + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + assert_difference "TrafficLight.count" do + ActiveRecord::Base.transaction do + conn = ActiveRecord::Base.connection + assert_equal 1, conn.open_transactions + conn.insert_fixtures_set(fixtures) + assert_equal 1, conn.open_transactions + end + end + end + end + + if current_adapter?(:Mysql2Adapter) + def test_bulk_insert_with_multi_statements_enabled + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: %w[MULTI_STATEMENTS]) + ) + + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do + assert_nothing_raised do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + + assert_difference "TrafficLight.count" do + ActiveRecord::Base.transaction do + conn = ActiveRecord::Base.connection + assert_equal 1, conn.open_transactions + conn.insert_fixtures_set(fixtures) + assert_equal 1, conn.open_transactions + end + end + + assert_nothing_raised do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + end + end + end + + def test_bulk_insert_with_multi_statements_disabled + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: []) + ) + + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do + assert_raises(ActiveRecord::StatementInvalid) do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + + assert_difference "TrafficLight.count" do + conn = ActiveRecord::Base.connection + conn.insert_fixtures_set(fixtures) + end + + assert_raises(ActiveRecord::StatementInvalid) do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + end + end + end + + def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size + conn = ActiveRecord::Base.connection + mysql_margin = 2 + packet_size = 1024 + bytes_needed_to_have_a_1024_bytes_fixture = 858 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] }, + ] + } + + conn.stub(:max_allowed_packet, packet_size - mysql_margin) do + error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) } + assert_match(/Fixtures set is too large #{packet_size}\./, error.message) + end + end + + def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] }, + ] + } + + conn.stub(:max_allowed_packet, packet_size) do + assert_difference "TrafficLight.count" do + conn.insert_fixtures_set(fixtures) + end + end + end + + def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 450 }, + ] + } + + conn.stub(:max_allowed_packet, packet_size) do + conn.insert_fixtures_set(fixtures) + + assert_equal 2, subscriber.events.size + assert_operator subscriber.events.first.bytesize, :<, packet_size + assert_operator subscriber.events.second.bytesize, :<, packet_size + end + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 200 }, + ] + } + + conn.stub(:max_allowed_packet, packet_size) do + assert_difference ["TrafficLight.count", "Comment.count"], +1 do + conn.insert_fixtures_set(fixtures) + end + end + assert_equal 1, subscriber.events.size + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + end + + def test_auto_value_on_primary_key + fixtures = [ + { "name" => "first", "wheels_count" => 2 }, + { "name" => "second", "wheels_count" => 3 } + ] + conn = ActiveRecord::Base.connection + assert_nothing_raised do + conn.insert_fixtures_set({ "aircraft" => fixtures }, ["aircraft"]) + end + result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") + assert_equal fixtures, result.to_a + end + + def test_deprecated_insert_fixtures + fixtures = [ + { "name" => "first", "wheels_count" => 2 }, + { "name" => "second", "wheels_count" => 3 } + ] + conn = ActiveRecord::Base.connection + conn.delete("DELETE FROM aircraft") + assert_deprecated do + conn.insert_fixtures(fixtures, "aircraft") + end + result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") + assert_equal fixtures, result.to_a end def test_broken_yaml_exception @@ -248,7 +484,7 @@ class FixturesTest < ActiveRecord::TestCase nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere" # sanity check to make sure that this file never exists - assert Dir[nonexistent_fixture_path + "*"].empty? + assert_empty Dir[nonexistent_fixture_path + "*"] assert_raise(Errno::ENOENT) do ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) @@ -447,14 +683,14 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase fixtures :topics, :developers, :accounts def test_without_complete_instantiation - assert !defined?(@first) - assert !defined?(@topics) - assert !defined?(@developers) - assert !defined?(@accounts) + assert_not defined?(@first) + assert_not defined?(@topics) + assert_not defined?(@developers) + assert_not defined?(@accounts) end def test_fixtures_from_root_yml_without_instantiation - assert !defined?(@unknown), "@unknown is not defined" + assert_not defined?(@unknown), "@unknown is not defined" end def test_visibility_of_accessor_method @@ -489,7 +725,7 @@ class FixturesWithoutInstanceInstantiationTest < ActiveRecord::TestCase fixtures :topics, :developers, :accounts def test_without_instance_instantiation - assert !defined?(@first), "@first is not defined" + assert_not defined?(@first), "@first is not defined" end end @@ -688,44 +924,58 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase self.use_instantiated_fixtures = false def test_transaction_created_on_connection_notification - connection = stub(transaction_open?: false) - connection.expects(:begin_transaction).with(joinable: false) - pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec)) - pool.stubs(:lock_thread=).with(false) - fire_connection_notification(connection) + connection = Class.new do + attr_accessor :pool + + def transaction_open?; end + def begin_transaction(*args); end + def rollback_transaction(*args); end + end.new + + connection.pool = Class.new do + def lock_thread=(lock_thread); end + end.new + + assert_called_with(connection, :begin_transaction, [joinable: false]) do + fire_connection_notification(connection) + end end def test_notification_established_transactions_are_rolled_back - # Mocha is not thread-safe so define our own stub to test connection = Class.new do attr_accessor :rollback_transaction_called attr_accessor :pool + def transaction_open?; true; end def begin_transaction(*args); end def rollback_transaction(*args) @rollback_transaction_called = true end end.new + connection.pool = Class.new do - def lock_thread=(lock_thread); false; end + def lock_thread=(lock_thread); end end.new + fire_connection_notification(connection) teardown_fixtures + assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not") end private def fire_connection_notification(connection) - ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with("book").returns(connection) - message_bus = ActiveSupport::Notifications.instrumenter - payload = { - spec_name: "book", - config: nil, - connection_id: connection.object_id - } + assert_called_with(ActiveRecord::Base.connection_handler, :retrieve_connection, ["book"], returns: connection) do + message_bus = ActiveSupport::Notifications.instrumenter + payload = { + spec_name: "book", + config: nil, + connection_id: connection.object_id + } - message_bus.instrument("!connection.active_record", payload) {} + message_bus.instrument("!connection.active_record", payload) {} + end end end @@ -937,13 +1187,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase def test_supports_inline_habtm assert(parrots(:george).treasures.include?(treasures(:diamond))) assert(parrots(:george).treasures.include?(treasures(:sapphire))) - assert(!parrots(:george).treasures.include?(treasures(:ruby))) + assert_not(parrots(:george).treasures.include?(treasures(:ruby))) end def test_supports_inline_habtm_with_specified_id assert(parrots(:polly).treasures.include?(treasures(:ruby))) assert(parrots(:polly).treasures.include?(treasures(:sapphire))) - assert(!parrots(:polly).treasures.include?(treasures(:diamond))) + assert_not(parrots(:polly).treasures.include?(treasures(:diamond))) end def test_supports_yaml_arrays @@ -998,10 +1248,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_resolves_enums - assert books(:awdr).published? - assert books(:awdr).read? - assert books(:rfr).proposed? - assert books(:ddd).published? + assert_predicate books(:awdr), :published? + assert_predicate books(:awdr), :read? + assert_predicate books(:rfr), :proposed? + assert_predicate books(:ddd), :published? end end @@ -1041,7 +1291,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase end def test_table_name_is_defined_in_the_model - assert_equal "randomly_named_table2", ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name + assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name end end diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb index 5e503272e1..b15e1b48c4 100644 --- a/activerecord/test/cases/habtm_destroy_order_test.rb +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -15,7 +15,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase sicp.destroy end end - assert !sicp.destroyed? + assert_not_predicate sicp, :destroyed? end test "should not raise error if have foreign key in the join table" do @@ -42,7 +42,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase ben.lessons << sicp ben.save! ben.destroy - assert !ben.reload.lessons.empty? + assert_not_empty ben.reload.lessons ensure # get rid of it so Student is still like it was Student.reset_callbacks(:destroy) @@ -58,6 +58,6 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase assert_raises LessonError do sicp.destroy end - assert !sicp.reload.students.empty? + assert_not_empty sicp.reload.students end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 6ea02ac191..66f11fe5bd 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -184,4 +184,4 @@ module InTimeZone end end -require "mocha/setup" # FIXME: stop using mocha +require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index ff4385c8b4..3d3189900f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -91,7 +91,6 @@ class InheritanceTest < ActiveRecord::TestCase end ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do - exception = assert_raises NameError do Company.send :compute_type, "InvalidModel" end @@ -130,61 +129,70 @@ class InheritanceTest < ActiveRecord::TestCase end def test_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? + assert_not_predicate ActiveRecord::Base, :descends_from_active_record? # Abstract subclass of AR::Base. - assert LoosePerson.descends_from_active_record? + assert_predicate LoosePerson, :descends_from_active_record? # Concrete subclass of an abstract class. - assert LooseDescendant.descends_from_active_record? + assert_predicate LooseDescendant, :descends_from_active_record? # Concrete subclass of AR::Base. - assert TightPerson.descends_from_active_record? + assert_predicate TightPerson, :descends_from_active_record? # Concrete subclass of a concrete class but has no type column. - assert TightDescendant.descends_from_active_record? + assert_predicate TightDescendant, :descends_from_active_record? # Concrete subclass of AR::Base. - assert Post.descends_from_active_record? + assert_predicate Post, :descends_from_active_record? # Concrete subclasses of a concrete class which has a type column. - assert !StiPost.descends_from_active_record? - assert !SubStiPost.descends_from_active_record? + assert_not_predicate StiPost, :descends_from_active_record? + assert_not_predicate SubStiPost, :descends_from_active_record? # Abstract subclass of a concrete class which has a type column. # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !AbstractStiPost.descends_from_active_record? + assert_not_predicate AbstractStiPost, :descends_from_active_record? # Concrete subclass of an abstract class which has a type column. - assert !SubAbstractStiPost.descends_from_active_record? + assert_not_predicate SubAbstractStiPost, :descends_from_active_record? end def test_company_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? + assert_not_predicate ActiveRecord::Base, :descends_from_active_record? assert AbstractCompany.descends_from_active_record?, "AbstractCompany should descend from ActiveRecord::Base" assert Company.descends_from_active_record?, "Company should descend from ActiveRecord::Base" - assert !Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base" + assert_not Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base" end def test_abstract_class - assert !ActiveRecord::Base.abstract_class? - assert LoosePerson.abstract_class? - assert !LooseDescendant.abstract_class? + assert_not_predicate ActiveRecord::Base, :abstract_class? + assert_predicate LoosePerson, :abstract_class? + assert_not_predicate LooseDescendant, :abstract_class? end def test_inheritance_base_class assert_equal Post, Post.base_class + assert_predicate Post, :base_class? assert_equal Post, SpecialPost.base_class + assert_not_predicate SpecialPost, :base_class? assert_equal Post, StiPost.base_class + assert_not_predicate StiPost, :base_class? assert_equal Post, SubStiPost.base_class + assert_not_predicate SubStiPost, :base_class? assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class + assert_predicate SubAbstractStiPost, :base_class? end def test_abstract_inheritance_base_class assert_equal LoosePerson, LoosePerson.base_class + assert_predicate LoosePerson, :base_class? assert_equal LooseDescendant, LooseDescendant.base_class + assert_predicate LooseDescendant, :base_class? assert_equal TightPerson, TightPerson.base_class + assert_predicate TightPerson, :base_class? assert_equal TightPerson, TightDescendant.base_class + assert_not_predicate TightDescendant, :base_class? end def test_base_class_activerecord_error diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index ebe0b0aa87..363beb4780 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -215,7 +215,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert @@ -223,11 +223,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert_by_part @@ -241,12 +241,12 @@ module ActiveRecord } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") assert migration.connection.table_exists?("new_horses") migration.migrate :down assert_equal [:both, :up, :both, :down], received assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + assert_not migration.connection.table_exists?("new_horses") end def test_migrate_revert_whole_migration @@ -255,11 +255,11 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end end @@ -268,7 +268,7 @@ module ActiveRecord revert.migrate :down assert revert.connection.table_exists?("horses") revert.migrate :up - assert !revert.connection.table_exists?("horses") + assert_not revert.connection.table_exists?("horses") end def test_migrate_revert_change_column_default @@ -341,7 +341,7 @@ module ActiveRecord def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_up @@ -352,7 +352,7 @@ module ActiveRecord def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_migrate_down_with_table_name_prefix @@ -361,7 +361,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + assert_not ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = "" end @@ -383,7 +383,7 @@ module ActiveRecord 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"), + assert_not connection.index_exists?(:horses, :content, name: "horses_index_named"), "horses_index_named index should not exist" end end @@ -402,7 +402,7 @@ module ActiveRecord UpOnlyMigration.new.migrate(:down) # should be no error connection = ActiveRecord::Base.connection - assert !connection.column_exists?(:horses, :oldie) + assert_not connection.column_exists?(:horses, :oldie) Horse.reset_column_information end end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 52fe488cd5..82cf281cff 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -252,7 +252,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase def @david.favorite_quote; "Constraints are liberating"; end json = @david.to_json(include: :posts, methods: :favorite_quote) - assert !@david.posts.first.respond_to?(:favorite_quote) + assert_not_respond_to @david.posts.first, :favorite_quote assert_match %r{"favorite_quote":"Constraints are liberating"}, json assert_equal 1, %r{"favorite_quote":}.match(json).size end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index b0c0f2c283..9b79803503 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -26,7 +26,7 @@ module JSONSharedTestCases assert_type_match column_type, column.sql_type type = klass.type_for_attribute("payload") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_change_table_supports_json @@ -101,7 +101,7 @@ module JSONSharedTestCases x = klass.where(payload: nil).first assert_nil(x) - json.update_attributes(payload: nil) + json.update(payload: nil) x = klass.where(payload: nil).first assert_equal(json.reload, x) end @@ -152,42 +152,42 @@ module JSONSharedTestCases def test_changes_in_place json = klass.new - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? + assert_predicate json, :changed? + assert_predicate json, :payload_changed? json.save! - assert_not json.changed? + assert_not_predicate json, :changed? json.payload["three"] = "four" - assert json.payload_changed? + assert_predicate json, :payload_changed? json.save! json.reload assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? + assert_not_predicate json, :changed? end def test_changes_in_place_ignores_key_order json = klass.new - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = { "three" => "four", "one" => "two" } json.save! json.reload json.payload = { "three" => "four", "one" => "two" } - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] json.save! json.reload json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] - assert_not json.changed? + assert_not_predicate json, :changed? end def test_changes_in_place_with_ruby_object @@ -195,10 +195,10 @@ module JSONSharedTestCases json = klass.create!(payload: time) json.reload - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = time - assert_not json.changed? + assert_not_predicate json, :changed? end def test_assigning_string_literal diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 3701be4b11..33bd74e114 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -15,6 +15,7 @@ require "models/bulb" require "models/engine" require "models/wheel" require "models/treasure" +require "models/frog" class LockWithoutDefault < ActiveRecord::Base; end @@ -69,8 +70,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StaleObjectError) { s2.destroy } assert s1.destroy - assert s1.frozen? - assert s1.destroyed? + assert_predicate s1, :frozen? + assert_predicate s1, :destroyed? assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") } end @@ -104,8 +105,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::StaleObjectError) { p2.destroy } assert p1.destroy - assert p1.frozen? - assert p1.destroyed? + assert_predicate p1, :frozen? + assert_predicate p1, :destroyed? assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) } end @@ -194,6 +195,45 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_update_with_dirty_primary_key + assert_raises(ActiveRecord::RecordNotUnique) do + person = Person.find(1) + person.id = 2 + person.save! + end + + person = Person.find(1) + person.id = 42 + person.save! + + assert Person.find(42) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + + def test_delete_with_dirty_primary_key + person = Person.find(1) + person.id = 2 + person.delete + + assert Person.find(2) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + + def test_destroy_with_dirty_primary_key + person = Person.find(1) + person.id = 2 + person.destroy + + assert Person.find(2) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + def test_explicit_update_lock_column_raise_error person = Person.find(1) @@ -201,7 +241,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase person.first_name = "Douglas Adams" person.lock_version = 42 - assert person.lock_version_changed? + assert_predicate person, :lock_version_changed? person.save end @@ -405,30 +445,38 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, car.wheels_count assert_equal 0, car.lock_version - previously_car_updated_at = car.updated_at - travel(2.second) do + previously_updated_at = car.updated_at + previously_wheels_owned_at = car.wheels_owned_at + travel(1.second) do Wheel.create!(wheelable: car) end assert_equal 1, car.reload.wheels_count - assert_not_equal previously_car_updated_at, car.updated_at assert_equal 1, car.lock_version + assert_operator previously_updated_at, :<, car.updated_at + assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at - previously_car_updated_at = car.updated_at - car.wheels.first.update(size: 42) + previously_updated_at = car.updated_at + previously_wheels_owned_at = car.wheels_owned_at + travel(2.second) do + car.wheels.first.update(size: 42) + end assert_equal 1, car.reload.wheels_count - assert_not_equal previously_car_updated_at, car.updated_at assert_equal 2, car.lock_version + assert_operator previously_updated_at, :<, car.updated_at + assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at - previously_car_updated_at = car.updated_at - travel(2.second) do + previously_updated_at = car.updated_at + previously_wheels_owned_at = car.wheels_owned_at + travel(3.second) do car.wheels.first.destroy! end assert_equal 0, car.reload.wheels_count - assert_not_equal previously_car_updated_at, car.updated_at assert_equal 3, car.lock_version + assert_operator previously_updated_at, :<, car.updated_at + assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at end def test_polymorphic_destroy_with_dependencies_and_lock_version @@ -440,16 +488,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_difference "car.wheels.count", -1 do car.reload.destroy end - assert car.destroyed? + assert_predicate car, :destroyed? end def test_removing_has_and_belongs_to_many_associations_upon_destroy p = RichPerson.create! first_name: "Jon" p.treasures.create! - assert !p.treasures.empty? + assert_not_empty p.treasures p.destroy - assert p.treasures.empty? - assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? + assert_empty p.treasures + assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1") end def test_yaml_dumping_with_lock_column @@ -519,7 +567,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase t1.destroy - assert t1.destroyed? + assert_predicate t1, :destroyed? end def test_destroy_stale_object @@ -532,7 +580,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase stale_object.destroy! end - refute stale_object.destroyed? + assert_not_predicate stale_object, :destroyed? end private @@ -612,6 +660,16 @@ unless in_memory_db? end end + def test_locking_in_after_save_callback + assert_nothing_raised do + frog = ::Frog.create(name: "Old Frog") + frog.name = "New Frog" + assert_not_deprecated do + frog.save! + end + end + end + def test_with_lock_commits_transaction person = Person.find 1 person.with_lock do diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index e2742ed33e..f0126fdb0d 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -177,11 +177,25 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.sql(Event.new(0, sql: "hi mom!")) + assert_equal 2, @logger.logged(:debug).size assert_match(/↳/, @logger.logged(:debug).last) ensure ActiveRecord::Base.verbose_query_logs = false end + def test_verbose_query_with_ignored_callstack + ActiveRecord::Base.verbose_query_logs = true + + logger = TestDebugLogSubscriber.new + def logger.extract_query_source_location(*); nil; end + + logger.sql(Event.new(0, sql: "hi mom!")) + assert_equal 1, @logger.logged(:debug).size + assert_no_match(/↳/, @logger.logged(:debug).last) + ensure + ActiveRecord::Base.verbose_query_logs = false + end + def test_verbose_query_logs_disabled_by_default logger = TestDebugLogSubscriber.new logger.sql(Event.new(0, sql: "hi mom!")) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 38a906c8f5..7777508349 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -84,7 +84,7 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array? + assert_predicate array_column, :array? end def test_create_table_with_array_column @@ -95,7 +95,7 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array? + assert_predicate array_column, :array? end end @@ -196,6 +196,17 @@ module ActiveRecord assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message end + def test_create_table_raises_when_defining_existing_column + error = assert_raise(ArgumentError) do + connection.create_table :testings do |t| + t.column :testing_column, :string + t.column :testing_column, :integer + end + end + + assert_equal "you can't define an already defined column 'testing_column'.", error.message + end + def test_create_table_with_timestamps_should_create_datetime_columns connection.create_table table_name do |t| t.timestamps @@ -205,8 +216,8 @@ module ActiveRecord created_at_column = created_columns.detect { |c| c.name == "created_at" } updated_at_column = created_columns.detect { |c| c.name == "updated_at" } - assert !created_at_column.null - assert !updated_at_column.null + assert_not created_at_column.null + assert_not updated_at_column.null end def test_create_table_with_timestamps_should_create_datetime_columns_with_options @@ -408,7 +419,7 @@ module ActiveRecord end connection.change_table :testings do |t| assert t.column_exists?(:foo) - assert !(t.column_exists?(:bar)) + assert_not (t.column_exists?(:bar)) end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 8ca20b6172..cedd9c44e3 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -67,7 +67,7 @@ module ActiveRecord if current_adapter?(:Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" - assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? + assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment? TestModel.reset_column_information ensure rename_column "test_models", "id_test", "id" @@ -223,31 +223,31 @@ module ActiveRecord def test_change_column_with_nil_default add_column "test_models", "contributor", :boolean, default: true - assert TestModel.new.contributor? + assert_predicate TestModel.new, :contributor? change_column "test_models", "contributor", :boolean, default: nil TestModel.reset_column_information - assert_not TestModel.new.contributor? + assert_not_predicate TestModel.new, :contributor? assert_nil TestModel.new.contributor end def test_change_column_to_drop_default_with_null_false add_column "test_models", "contributor", :boolean, default: true, null: false - assert TestModel.new.contributor? + assert_predicate TestModel.new, :contributor? change_column "test_models", "contributor", :boolean, default: nil, null: false TestModel.reset_column_information - assert_not TestModel.new.contributor? + assert_not_predicate TestModel.new, :contributor? assert_nil TestModel.new.contributor end def test_change_column_with_new_default add_column "test_models", "administrator", :boolean, default: true - assert TestModel.new.administrator? + assert_predicate TestModel.new, :administrator? change_column "test_models", "administrator", :boolean, default: false TestModel.reset_column_information - assert_not TestModel.new.administrator? + assert_not_predicate TestModel.new, :administrator? end def test_change_column_with_custom_index_name diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 58bc558619..3a11bb081b 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -14,7 +14,7 @@ module ActiveRecord recorder = CommandRecorder.new(Class.new { def america; end }.new) - assert recorder.respond_to?(:america) + assert_respond_to recorder, :america end def test_send_calls_super @@ -27,7 +27,7 @@ module ActiveRecord recorder = CommandRecorder.new(Class.new { def create_table(name); end }.new) - assert recorder.respond_to?(:create_table), "respond_to? create_table" + assert_respond_to recorder, :create_table recorder.send(:create_table, :horses) assert_equal [[:create_table, [:horses], nil]], recorder.commands end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 26d3b3e29d..69a50674af 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -182,7 +182,7 @@ module LegacyPrimaryKeyTestCases assert_legacy_primary_key legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] - assert_not legacy_ref.bigint? + assert_not_predicate legacy_ref, :bigint? record1 = LegacyPrimaryKey.create! assert_not_nil record1.id @@ -301,8 +301,8 @@ module LegacyPrimaryKeyTestCases @migration.migrate(:up) legacy_pk = LegacyPrimaryKey.columns_hash["id"] - assert legacy_pk.bigint? - assert legacy_pk.auto_increment? + assert_predicate legacy_pk, :bigint? + assert_predicate legacy_pk, :auto_increment? schema = dump_table_schema "legacy_primary_keys" assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema @@ -334,7 +334,7 @@ module LegacyPrimaryKeyTestCases legacy_pk = LegacyPrimaryKey.columns_hash["id"] assert_equal :integer, legacy_pk.type - assert_not legacy_pk.bigint? + assert_not_predicate legacy_pk, :bigint? assert_not legacy_pk.null if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 77d32a24a5..e0cbb29dcf 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -69,7 +69,7 @@ module ActiveRecord def test_create_join_table_without_indexes connection.create_join_table :artists, :musics - assert connection.indexes(:artists_musics).blank? + assert_predicate connection.indexes(:artists_musics), :blank? end def test_create_join_table_with_index @@ -95,42 +95,42 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table "artists", "musics" - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.table_exists?("musics_videos") + assert_not connection.table_exists?("musics_videos") end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: "catalog" connection.drop_join_table :artists, :musics, table_name: "catalog" - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: { null: true } connection.drop_join_table :artists, :musics, column_options: { null: true } - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_create_and_drop_join_table_with_common_prefix @@ -139,7 +139,7 @@ module ActiveRecord assert connection.table_exists?("audio_artists_musics") connection.drop_join_table "audio_artists", "audio_musics" - assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" + assert_not connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 079be04946..c471dd1106 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -19,6 +19,52 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) end end + + class ForeignKeyChangeColumnTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class Rocket < ActiveRecord::Base + has_many :astronauts + end + + class Astronaut < ActiveRecord::Base + belongs_to :rocket + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets", force: true do |t| + t.string :name + end + + @connection.create_table "astronauts", force: true do |t| + t.string :name + t.references :rocket, foreign_key: true + end + Rocket.reset_column_information + Astronaut.reset_column_information + end + + teardown do + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true + Rocket.reset_column_information + Astronaut.reset_column_information + end + + def test_change_column_of_parent_table + foreign_keys = ActiveRecord::Base.connection.foreign_keys("astronauts") + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.change_column_null :rockets, :name, false + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + end end end end @@ -235,39 +281,39 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal 1, foreign_keys.size fk = foreign_keys.first - refute fk.validated? + assert_not_predicate fk, :validated? end def test_validate_foreign_key_infers_column @connection.add_foreign_key :astronauts, :rockets, validate: false - refute @connection.foreign_keys("astronauts").first.validated? + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? @connection.validate_foreign_key :astronauts, :rockets - assert @connection.foreign_keys("astronauts").first.validated? + assert_predicate @connection.foreign_keys("astronauts").first, :validated? end def test_validate_foreign_key_by_column @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false - refute @connection.foreign_keys("astronauts").first.validated? + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? @connection.validate_foreign_key :astronauts, column: "rocket_id" - assert @connection.foreign_keys("astronauts").first.validated? + assert_predicate @connection.foreign_keys("astronauts").first, :validated? end def test_validate_foreign_key_by_symbol_column @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false - refute @connection.foreign_keys("astronauts").first.validated? + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? @connection.validate_foreign_key :astronauts, column: :rocket_id - assert @connection.foreign_keys("astronauts").first.validated? + assert_predicate @connection.foreign_keys("astronauts").first, :validated? end def test_validate_foreign_key_by_name @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false - refute @connection.foreign_keys("astronauts").first.validated? + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? @connection.validate_foreign_key :astronauts, name: "fancy_named_fk" - assert @connection.foreign_keys("astronauts").first.validated? + assert_predicate @connection.foreign_keys("astronauts").first, :validated? end def test_validate_foreign_non_existing_foreign_key_raises @@ -280,7 +326,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false @connection.validate_constraint :astronauts, "fancy_named_fk" - assert @connection.foreign_keys("astronauts").first.validated? + assert_predicate @connection.foreign_keys("astronauts").first, :validated? end else # Foreign key should still be created, but should not be invalid @@ -291,7 +337,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal 1, foreign_keys.size fk = foreign_keys.first - assert fk.validated? + assert_predicate fk, :validated? end end @@ -306,6 +352,17 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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_with_custom_fk_ignore_pattern + original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern + ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/ + @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + + ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern + 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 diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b25c6d84bc..f8fecc83cd 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -99,7 +99,7 @@ module ActiveRecord connection.add_index :testings, :foo assert connection.index_exists?(:testings, :foo) - assert !connection.index_exists?(:testings, :bar) + assert_not connection.index_exists?(:testings, :bar) end def test_index_exists_on_multiple_columns @@ -131,15 +131,18 @@ module ActiveRecord assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, name: "custom_index_name") - assert !connection.index_exists?(:testings, :foo, name: "other_index_name") + assert_not connection.index_exists?(:testings, :foo, name: "other_index_name") end def test_remove_named_index - connection.add_index :testings, :foo, name: "custom_index_name" + connection.add_index :testings, :foo, name: "index_testings_on_custom_index_name" assert connection.index_exists?(:testings, :foo) + + assert_raise(ArgumentError) { connection.remove_index(:testings, "custom_index_name") } + connection.remove_index :testings, :foo - assert !connection.index_exists?(:testings, :foo) + assert_not connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -203,7 +206,7 @@ module ActiveRecord assert connection.index_exists?("testings", "last_name") connection.remove_index("testings", "last_name") - assert !connection.index_exists?("testings", "last_name") + assert_not connection.index_exists?("testings", "last_name") end end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index d0066f68be..dedb5ea502 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -4,37 +4,37 @@ require "cases/helper" 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 + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class PendingMigrationsTest < ActiveRecord::TestCase + setup do + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid" + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end - def teardown - assert @connection.verify - assert @app.verify - super - end + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end + + def test_errors_if_pending + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - def test_errors_if_pending - ActiveRecord::Migrator.stub :needs_migration?, true do - assert_raise ActiveRecord::PendingMigrationError do - @pending.call(nil) + assert_raises ActiveRecord::PendingMigrationError do + CheckPending.new(Proc.new {}).call({}) end end - end - def test_checks_if_supported - @app.expect :call, nil, [:foo] + def test_checks_if_supported + ActiveRecord::SchemaMigration.create_table + migrator = Base.connection.migration_context + capture(:stdout) { migrator.migrate } - ActiveRecord::Migrator.stub :needs_migration?, false do - @pending.call(:foo) + assert_nil CheckPending.new(Proc.new {}).call({}) end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index dfce266253..a9deb92585 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -86,9 +86,9 @@ module ActiveRecord rename_table :cats, :felines assert connection.table_exists? :felines - refute connection.table_exists? :cats + assert_not connection.table_exists? :cats - primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0] SELECT c.relname FROM pg_class c JOIN pg_index i @@ -107,7 +107,7 @@ module ActiveRecord connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines - refute connection.table_exists? :cats + assert_not connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index a3ebc8070a..d1292dc53d 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -71,6 +71,16 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end + def test_migrator_migrations_path_is_deprecated + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "/whatever" + end + ensure + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "db/migrate" + end + end + def test_migration_version_matches_component_version assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version end @@ -78,20 +88,20 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) - assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal false, ActiveRecord::Migrator.needs_migration? + migrator.up + assert_equal 3, migrator.current_version + assert_equal false, migrator.needs_migration? - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") - assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal true, ActiveRecord::Migrator.needs_migration? + migrator.down + assert_equal 0, migrator.current_version + assert_equal true, migrator.needs_migration? ActiveRecord::SchemaMigration.create!(version: 3) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_detection_without_schema_migration_table @@ -99,28 +109,31 @@ class MigrationTest < ActiveRecord::TestCase migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_any_migrations old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid" + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") - assert ActiveRecord::Migrator.any_migrations? + assert_predicate migrator, :any_migrations? - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty" + migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") - assert_not ActiveRecord::Migrator.any_migrations? + assert_not_predicate migrator_empty, :any_migrations? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_version - assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check") + assert_equal 0, migrator.current_version + migrator.up(20131219224947) + assert_equal 20131219224947, migrator.current_version end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -157,14 +170,14 @@ class MigrationTest < ActiveRecord::TestCase def test_add_table_with_decimals Person.connection.drop_table :big_numbers rescue nil - assert !BigNumber.table_exists? + assert_not_predicate BigNumber, :table_exists? GiveMeBigNumbers.up BigNumber.reset_column_information assert BigNumber.create( bank_balance: 1586.43, big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3, value_of_e: BigDecimal("2.7182818284590452353602875") ) @@ -178,10 +191,8 @@ class MigrationTest < ActiveRecord::TestCase assert_not_nil b.my_house_population assert_not_nil b.value_of_e - # TODO: set world_population >= 2**62 to cover 64-bit platforms and test - # is_a?(Bignum) assert_kind_of Integer, b.world_population - assert_equal 6000000000, b.world_population + assert_equal 2**62, b.world_population assert_kind_of Integer, b.my_house_population assert_equal 3, b.my_house_population assert_kind_of BigDecimal, b.bank_balance @@ -216,15 +227,16 @@ class MigrationTest < ActiveRecord::TestCase def test_filtering_migrations assert_no_column Person, :last_name - assert !Reminder.table_exists? + assert_not_predicate Reminder, :table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") + migrator.up(&name_filter) assert_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator.down(&name_filter) assert_no_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } @@ -250,21 +262,21 @@ class MigrationTest < ActiveRecord::TestCase def test_instance_based_migration_up migration = MockMigration.new - assert !migration.went_up, "have not gone up" - assert !migration.went_down, "have not gone down" + assert_not migration.went_up, "have not gone up" + assert_not migration.went_down, "have not gone down" migration.migrate :up assert migration.went_up, "have gone up" - assert !migration.went_down, "have not gone down" + assert_not migration.went_down, "have not gone down" end def test_instance_based_migration_down migration = MockMigration.new - assert !migration.went_up, "have not gone up" - assert !migration.went_down, "have not gone down" + assert_not migration.went_up, "have not gone up" + assert_not migration.went_down, "have not gone down" migration.migrate :down - assert !migration.went_up, "have gone up" + assert_not migration.went_up, "have gone up" assert migration.went_down, "have not gone down" end @@ -382,9 +394,9 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] original_rails_env = ENV["RAILS_ENV"] @@ -392,16 +404,16 @@ class MigrationTest < ActiveRecord::TestCase ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - refute_equal current_env, new_env + assert_not_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal new_env, ActiveRecord::InternalMetadata[:environment] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env - ActiveRecord::Migrator.up(migrations_path) + migrator.up end def test_internal_metadata_stores_environment_when_other_data_exists @@ -411,14 +423,15 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - ActiveRecord::Migrator.up(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) + migrator.up end def test_proper_table_name_on_migration @@ -450,7 +463,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_rename_table_with_prefix_and_suffix - assert !Thing.table_exists? + assert_not_predicate Thing, :table_exists? ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" Thing.reset_table_name @@ -471,7 +484,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_add_drop_table_with_prefix_and_suffix - assert !Reminder.table_exists? + assert_not_predicate Reminder, :table_exists? ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name @@ -535,7 +548,7 @@ class MigrationTest < ActiveRecord::TestCase end assert Person.connection.column_exists?(:something, :foo) assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar } - assert !Person.connection.column_exists?(:something, :foo) + assert_not Person.connection.column_exists?(:something, :foo) assert Person.connection.column_exists?(:something, :name) assert Person.connection.column_exists?(:something, :number) ensure @@ -678,6 +691,25 @@ class MigrationTest < ActiveRecord::TestCase assert_no_column Person, :last_name, "without an advisory lock, the Migrator should not make any changes, but it did." end + + def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + e = assert_raises(ActiveRecord::ConcurrentMigrationError) do + silence_stream($stderr) do + migrator.send(:with_advisory_lock) do + ActiveRecord::Base.connection.release_advisory_lock(lock_id) + end + end + end + + assert_match( + /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/, + e.message + ) + end end private @@ -790,7 +822,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - [:qualification, :experience].each { |c| assert ! column(c) } + [:qualification, :experience].each { |c| assert_not column(c) } assert column(:qualification_experience) end @@ -820,7 +852,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? name_age_index = index(:index_delete_me_on_name_and_age) assert_equal ["name", "age"].sort, name_age_index.columns.sort - assert ! name_age_index.unique + assert_not name_age_index.unique assert index(:awesome_username_index).unique end @@ -848,7 +880,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - assert ! index(:index_delete_me_on_name) + assert_not index(:index_delete_me_on_name) new_name_index = index(:new_name_index) assert new_name_index.unique @@ -860,13 +892,13 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.date :birthdate end - assert ! column(:name).default + assert_not column(:name).default assert_equal :date, column(:birthdate).type classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change - "PostgreSQLAdapter" => 2, # one query for columns, one for bulk change + "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment }.fetch(classname) { raise "need an expected query count for #{classname}" } @@ -874,12 +906,13 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_queries(expected_query_count, ignore_none: true) do with_bulk_change_table do |t| t.change :name, :string, default: "NONAME" - t.change :birthdate, :datetime + t.change :birthdate, :datetime, comment: "This is a comment" end end assert_equal "NONAME", column(:name).default assert_equal :datetime, column(:birthdate).type + assert_equal "This is a comment", column(:birthdate).comment end private @@ -939,7 +972,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied ensure clear end @@ -980,7 +1013,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied end ensure clear @@ -1022,7 +1055,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied end ensure clear @@ -1043,7 +1076,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied ensure clear end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1047ba1367..873455cf67 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -89,7 +89,7 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -98,7 +98,8 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_subdirectories - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories") + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations + [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -108,7 +109,7 @@ class MigratorTest < ActiveRecord::TestCase def test_finds_migrations_from_two_directories directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] - migrations = ActiveRecord::Migrator.migrations directories + migrations = ActiveRecord::MigrationContext.new(directories).migrations [[20090101010101, "PeopleHaveHobbies"], [20090101010202, "PeopleHaveDescriptions"], @@ -121,14 +122,14 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_numbered_directory - migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + "/10_urban"] + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations assert_equal 9, migrations[0].version assert_equal "AddExpressions", migrations[0].name end def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid") + ActiveRecord::MigrationContext.new("valid").migrations end migration_proxy = list.find { |item| @@ -157,7 +158,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_in_subdirectories @@ -171,7 +172,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_with_schema_define_in_subdirectories @@ -186,7 +187,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["up", "003", "Innocent jointable"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status ensure ActiveRecord::Migrator.migrations_paths = prev_paths end @@ -204,7 +205,7 @@ class MigratorTest < ActiveRecord::TestCase ["down", "20100201010101", "Valid with timestamps we need reminders"], ["down", "20100301010101", "Valid with timestamps innocent jointable"], ["up", "20160528010101", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(paths) + ], ActiveRecord::MigrationContext.new(paths).migrations_status end def test_migrator_interleaved_migrations @@ -232,25 +233,28 @@ class MigratorTest < ActiveRecord::TestCase def test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:up, migrations).migrate + migrator = ActiveRecord::Migrator.new(:up, migrations) + migrator.migrate assert migrations.all?(&:went_up) assert migrations.all? { |m| !m.went_down } - assert_equal 2, ActiveRecord::Migrator.current_version + assert_equal 2, migrator.current_version end def test_down_calls_down test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:down, migrations).migrate + migrator = ActiveRecord::Migrator.new(:down, migrations) + migrator.migrate assert migrations.all? { |m| !m.went_up } assert migrations.all?(&:went_down) - assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 0, migrator.current_version end def test_current_version ActiveRecord::SchemaMigration.create!(version: "1000") - assert_equal 1000, ActiveRecord::Migrator.current_version + migrator = ActiveRecord::MigrationContext.new("db/migrate") + assert_equal 1000, migrator.current_version end def test_migrator_one_up @@ -289,33 +293,36 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_double_up calls, migrations = sensors(3) - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) + assert_equal(0, migrator.current_version) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [], calls end def test_migrator_double_down calls, migrations = sensors(3) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version - ActiveRecord::Migrator.new(:up, migrations, 1).run + migrator.run assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator = ActiveRecord::Migrator.new(:down, migrations, 1) + migrator.run assert_equal [[:down, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator.run assert_equal [], calls - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version end def test_migrator_verbosity @@ -361,78 +368,85 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_going_down_due_to_version_target calls, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.up("valid", 1) + migrator.up(1) assert_equal [[:up, 1]], calls calls.clear - migrator.migrate("valid", 0) + migrator.migrate(0) assert_equal [[:down, 1]], calls calls.clear - migrator.migrate("valid") + migrator.migrate assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end def test_migrator_output_when_running_multiple_migrations _, migrator = migrator_class(3) + migrator = migrator.new("valid") - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(3, result.count) # Nothing migrated from duplicate run - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(0, result.count) - result = migrator.rollback("valid") + result = migrator.rollback assert_equal(1, result.count) end def test_migrator_output_when_running_single_migration _, migrator = migrator_class(1) - result = migrator.run(:up, "valid", 1) + migrator = migrator.new("valid") + + result = migrator.run(:up, 1) assert_equal(1, result.version) end def test_migrator_rollback _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.migrate + assert_equal(3, migrator.current_version) - migrator.rollback("valid") - assert_equal(2, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(2, migrator.current_version) - migrator.rollback("valid") - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(1, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) end def test_migrator_db_has_no_schema_migrations_table _, migrator = migrator_class(3) + migrator = migrator.new("valid") ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") - migrator.migrate("valid", 1) + migrator.migrate(1) assert ActiveRecord::Base.connection.table_exists?("schema_migrations") end def test_migrator_forward _, migrator = migrator_class(3) - migrator.migrate("/valid", 1) - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator = migrator.new("/valid") + migrator.migrate(1) + assert_equal(1, migrator.current_version) - migrator.forward("/valid", 2) - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward(2) + assert_equal(3, migrator.current_version) - migrator.forward("/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward + assert_equal(3, migrator.current_version) end def test_only_loads_pending_migrations @@ -440,25 +454,27 @@ class MigratorTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.create!(version: "1") calls, migrator = migrator_class(3) - migrator.migrate("valid", nil) + migrator = migrator.new("valid") + migrator.migrate assert_equal [[:up, 2], [:up, 3]], calls end def test_get_all_versions _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions) + migrator.migrate + assert_equal([1, 2, 3], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1, 2], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([], migrator.get_all_versions) end private @@ -483,11 +499,11 @@ class MigratorTest < ActiveRecord::TestCase def migrator_class(count) calls, migrations = sensors(count) - migrator = Class.new(ActiveRecord::Migrator).extend(Module.new { - define_method(:migrations) { |paths| + migrator = Class.new(ActiveRecord::MigrationContext) { + define_method(:migrations) { |*| migrations } - }) + } [calls, migrator] end end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 060d555607..87455e4fcb 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -32,7 +32,7 @@ class ModulesTest < ActiveRecord::TestCase def test_module_spanning_associations firm = MyApplication::Business::Firm.first - assert !firm.clients.empty?, "Firm should have clients" + assert_not firm.clients.empty?, "Firm should have clients" assert_nil firm.class.table_name.match("::"), "Firm shouldn't have the module appear in its table name" end @@ -155,7 +155,7 @@ class ModulesTest < ActiveRecord::TestCase ActiveRecord::Base.store_full_sti_class = true collection = Shop::Collection.first - assert !collection.products.empty?, "Collection should have products" + assert_not collection.products.empty?, "Collection should have products" assert_nothing_raised { collection.destroy } ensure ActiveRecord::Base.store_full_sti_class = old @@ -166,7 +166,7 @@ class ModulesTest < ActiveRecord::TestCase ActiveRecord::Base.store_full_sti_class = true product = Shop::Product.first - assert !product.variants.empty?, "Product should have variants" + assert_not product.variants.empty?, "Product should have variants" assert_nothing_raised { product.destroy } ensure ActiveRecord::Base.store_full_sti_class = old diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index 59be4dc5a8..6f3903eed4 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -227,7 +227,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + assert_not_respond_to topic.written_on, :time_zone end end @@ -242,7 +242,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + assert_not_respond_to topic.written_on, :time_zone end ensure Topic.skip_time_zone_conversion_for_attributes = [] @@ -261,7 +261,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert_not topic.bonus_time.utc? + assert_not_predicate topic.bonus_time, :utc? attributes = { "written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "", @@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase "written_on(4i)" => "13", "written_on(5i)" => "55", ) - refute_predicate topic, :written_on_came_from_user? + assert_not_predicate topic, :written_on_came_from_user? end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index a2ccb603a9..aa519fd332 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -36,7 +36,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }] pirate.save! - assert pirate.birds_with_reject_all_blank.empty? + assert_empty pirate.birds_with_reject_all_blank end def test_should_not_build_a_new_record_if_reject_all_blank_returns_false @@ -44,7 +44,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }] pirate.save! - assert pirate.birds_with_reject_all_blank.empty? + assert_empty pirate.birds_with_reject_all_blank end def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false @@ -83,7 +83,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction ship = Ship.create!(name: "Nights Dirty Lightning") - assert !ship._destroy + assert_not ship._destroy ship.mark_for_destruction assert ship._destroy end @@ -152,7 +152,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man = Man.create(name: "Jon") interest = man.interests.create(topic: "the ladies") man.update(interests_attributes: { _destroy: "1", id: interest.id }) - assert man.reload.interests.empty? + assert_empty man.reload.interests end def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false @@ -240,7 +240,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @ship.destroy @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" } - assert !@pirate.ship.persisted? + assert_not_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name end @@ -261,7 +261,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" } - assert !@pirate.ship.persisted? + assert_not_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name assert_equal "Nights Dirty Lightning", @ship.name end @@ -335,7 +335,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_also_work_with_a_HashWithIndifferentAccess @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(id: @ship.id, name: "Davy Jones Gold Dagger") - assert @pirate.ship.persisted? + assert_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name end @@ -350,12 +350,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_destroy_the_associated_model_until_the_parent_is_saved @pirate.attributes = { ship_attributes: { id: @ship.id, _destroy: "1" } } - assert !@pirate.ship.destroyed? - assert @pirate.ship.marked_for_destruction? + assert_not_predicate @pirate.ship, :destroyed? + assert_predicate @pirate.ship, :marked_for_destruction? @pirate.save - assert @pirate.ship.destroyed? + assert_predicate @pirate.ship, :destroyed? assert_nil @pirate.reload.ship end @@ -424,7 +424,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.destroy @ship.reload.pirate_attributes = { catchphrase: "Arr" } - assert !@ship.pirate.persisted? + assert_not_predicate @ship.pirate, :persisted? assert_equal "Arr", @ship.pirate.catchphrase end @@ -445,7 +445,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @ship.reload.pirate_attributes = { catchphrase: "Arr" } - assert !@ship.pirate.persisted? + assert_not_predicate @ship.pirate, :persisted? assert_equal "Arr", @ship.pirate.catchphrase assert_equal "Aye", @pirate.catchphrase end @@ -550,7 +550,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.delete @ship.reload.attributes = { update_only_pirate_attributes: { catchphrase: "Arr" } } - assert !@ship.update_only_pirate.persisted? + assert_not_predicate @ship.update_only_pirate, :persisted? end def test_should_update_existing_when_update_only_is_true_and_no_id_is_given @@ -632,10 +632,10 @@ module NestedAttributesOnACollectionAssociationTests def test_should_not_load_association_when_updating_existing_records @pirate.reload @pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }]) - assert ! @pirate.send(@association_name).loaded? + assert_not_predicate @pirate.send(@association_name), :loaded? @pirate.save - assert ! @pirate.send(@association_name).loaded? + assert_not_predicate @pirate.send(@association_name), :loaded? assert_equal "Grace OMalley", @child_1.reload.name end @@ -663,13 +663,12 @@ module NestedAttributesOnACollectionAssociationTests def test_should_not_remove_scheduled_destroys_when_loading_association @pirate.reload @pirate.send(association_setter, [{ id: @child_1.id, _destroy: "1" }]) - assert @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.marked_for_destruction? + assert_predicate @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }, :marked_for_destruction? end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @child_1.stub(:id, "ABC1X") do @child_2.stub(:id, "ABC2X") do - @pirate.attributes = { association_getter => [ { id: @child_1.id, name: "Grace OMalley" }, @@ -705,10 +704,10 @@ module NestedAttributesOnACollectionAssociationTests association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } } } - assert !@pirate.send(@association_name).first.persisted? + assert_not_predicate @pirate.send(@association_name).first, :persisted? assert_equal "Grace OMalley", @pirate.send(@association_name).first.name - assert !@pirate.send(@association_name).last.persisted? + assert_not_predicate @pirate.send(@association_name).last, :persisted? assert_equal "Privateers Greed", @pirate.send(@association_name).last.name end @@ -835,7 +834,7 @@ module NestedAttributesOnACollectionAssociationTests man = Man.create(name: "John") interest = man.interests.create(topic: "bar", zine_id: 0) assert interest.save - assert !man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) + assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) end end @@ -1091,7 +1090,19 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR test "nested singular associations are validated" do part = ShipPart.new(name: "Stern", ship_attributes: { name: nil }) - assert_not part.valid? + assert_not_predicate part, :valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end end + +class TestNestedAttributesWithExtend < ActiveRecord::TestCase + setup do + Pirate.accepts_nested_attributes_for :treasures + end + + def test_extend_affects_nested_attributes + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + pirate.treasures_attributes = [{ id: nil }] + assert_equal "from extension", pirate.treasures[0].name + end +end diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index f04c68b08f..1d26057fdc 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -63,7 +63,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase # Characterizing when :before_add callback is called test ":before_add called for new bird when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = new_bird_attributes assert_new_bird_with_callback_called end @@ -80,7 +80,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase end test ":before_add not called for identical assignment when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = existing_birds_attributes assert_callbacks_not_called end @@ -92,7 +92,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase end test ":before_add not called for destroy assignment when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = destroy_bird_attributes assert_callbacks_not_called end @@ -111,7 +111,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase # Ensuring that the records in the association target are updated, # whether the association is loaded before or not test "Assignment updates records in target when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add) end @@ -124,7 +124,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase test("Assignment updates records in target when not loaded" \ " and callback loads target") do - assert_not @pirate.birds_with_add_load.loaded? + assert_not_predicate @pirate.birds_with_add_load, :loaded? @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add_load) end diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index f917c8f727..14db63890e 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -19,7 +19,7 @@ class NumericDataTest < ActiveRecord::TestCase m = NumericData.new( bank_balance: 1586.43, big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3 ) assert m.save @@ -27,11 +27,8 @@ class NumericDataTest < ActiveRecord::TestCase m1 = NumericData.find(m.id) assert_not_nil m1 - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population + assert_equal 2**62, m1.world_population assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population @@ -47,7 +44,7 @@ class NumericDataTest < ActiveRecord::TestCase m = NumericData.new( bank_balance: 1586.43122334, big_bank_balance: BigDecimal("234000567.952344"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3 ) assert m.save @@ -55,11 +52,8 @@ class NumericDataTest < ActiveRecord::TestCase m1 = NumericData.find(m.id) assert_not_nil m1 - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population + assert_equal 2**62, m1.world_population assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 0fa8ea212f..7348a22dd3 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -48,7 +48,7 @@ class PersistenceTest < ActiveRecord::TestCase end if test_update_with_order_succeeds.call("id DESC") - assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception + assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do @@ -206,18 +206,34 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal initial_credit + 2, a1.reload.credit_limit end - def test_increment_updates_timestamps + def test_increment_with_touch_updates_timestamps topic = topics(:first) - topic.update_columns(updated_at: 5.minutes.ago) - previous_updated_at = topic.updated_at - topic.increment!(:replies_count, touch: true) - assert_operator previous_updated_at, :<, topic.reload.updated_at + assert_equal 1, topic.replies_count + previously_updated_at = topic.updated_at + travel(1.second) do + topic.increment!(:replies_count, touch: true) + end + assert_equal 2, topic.reload.replies_count + assert_operator previously_updated_at, :<, topic.updated_at + end + + def test_increment_with_touch_an_attribute_updates_timestamps + topic = topics(:first) + assert_equal 1, topic.replies_count + previously_updated_at = topic.updated_at + previously_written_on = topic.written_on + travel(1.second) do + topic.increment!(:replies_count, touch: :written_on) + end + assert_equal 2, topic.reload.replies_count + assert_operator previously_updated_at, :<, topic.updated_at + assert_operator previously_written_on, :<, topic.written_on end def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a - assert ! topics_by_mary.empty? + assert_not_empty topics_by_mary assert_difference("Topic.count", -topics_by_mary.size) do destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) @@ -251,9 +267,16 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "The First Topic", topics(:first).becomes(Reply).title end + def test_becomes_after_reload_schema_from_cache + Reply.define_attribute_methods + Reply.serialize(:content) # invoke reload_schema_from_cache + assert_kind_of Reply, topics(:first).becomes(Reply) + assert_equal "The First Topic", topics(:first).becomes(Reply).title + end + def test_becomes_includes_errors company = Company.new(name: nil) - assert !company.valid? + assert_not_predicate company, :valid? original_errors = company.errors client = company.becomes(Client) assert_equal original_errors.keys, client.errors.keys @@ -283,6 +306,17 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "The First Topic", Topic.find(copy.id).title end + def test_becomes_wont_break_mutation_tracking + topic = topics(:first) + reply = topic.becomes(Reply) + + assert_equal 1, topic.id_in_database + assert_empty topic.attributes_in_database + + assert_equal 1, reply.id_in_database + assert_empty reply.attributes_in_database + end + def test_becomes_includes_changed_attributes company = Company.new(name: "37signals") client = company.becomes(Client) @@ -315,12 +349,28 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 41, accounts(:signals37, :reload).credit_limit end - def test_decrement_updates_timestamps + def test_decrement_with_touch_updates_timestamps topic = topics(:first) - topic.update_columns(updated_at: 5.minutes.ago) - previous_updated_at = topic.updated_at - topic.decrement!(:replies_count, touch: true) - assert_operator previous_updated_at, :<, topic.reload.updated_at + assert_equal 1, topic.replies_count + previously_updated_at = topic.updated_at + travel(1.second) do + topic.decrement!(:replies_count, touch: true) + end + assert_equal 0, topic.reload.replies_count + assert_operator previously_updated_at, :<, topic.updated_at + end + + def test_decrement_with_touch_an_attribute_updates_timestamps + topic = topics(:first) + assert_equal 1, topic.replies_count + previously_updated_at = topic.updated_at + previously_written_on = topic.written_on + travel(1.second) do + topic.decrement!(:replies_count, touch: :written_on) + end + assert_equal 0, topic.reload.replies_count + assert_operator previously_updated_at, :<, topic.updated_at + assert_operator previously_written_on, :<, topic.written_on end def test_create @@ -370,8 +420,8 @@ class PersistenceTest < ActiveRecord::TestCase developer.destroy new_developer = developer.dup new_developer.save - assert new_developer.persisted? - assert_not new_developer.destroyed? + assert_predicate new_developer, :persisted? + assert_not_predicate new_developer, :destroyed? end def test_create_many @@ -387,7 +437,7 @@ class PersistenceTest < ActiveRecord::TestCase ) topic = topic.dup # reset @new_record assert_nothing_raised { topic.save } - assert topic.persisted? + assert_predicate topic, :persisted? assert_equal "Another New Topic", topic.reload.title end @@ -435,7 +485,7 @@ class PersistenceTest < ActiveRecord::TestCase topic_reloaded = Topic.instantiate(topic.attributes.merge("does_not_exist" => "test")) topic_reloaded.title = "A New Topic" assert_nothing_raised { topic_reloaded.save } - assert topic_reloaded.persisted? + assert_predicate topic_reloaded, :persisted? assert_equal "A New Topic", topic_reloaded.reload.title end @@ -473,6 +523,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_instance_of Reply, Reply.find(reply.id) end + def test_becomes_default_sti_subclass + original_type = Topic.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply" + Topic.reset_column_information + + reply = topics(:second) + assert_instance_of Reply, reply + + topic = reply.becomes(Topic) + assert_instance_of Topic, topic + + ensure + ActiveRecord::Base.connection.change_column_default :topics, :type, original_type + Topic.reset_column_information + end + def test_update_after_create klass = Class.new(Topic) do def self.name; "Topic"; end @@ -502,7 +568,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_does_not_run_sql_if_record_has_not_changed topic = Topic.create(title: "Another New Topic") assert_queries(0) { assert topic.update(title: "Another New Topic") } - assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") } + assert_queries(0) { assert topic.update(title: "Another New Topic") } end def test_delete @@ -599,43 +665,65 @@ class PersistenceTest < ActiveRecord::TestCase end def test_delete_new_record - client = Client.new + client = Client.new(name: "37signals") client.delete - assert client.frozen? + assert_predicate client, :frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_delete_record_with_associations client = Client.find(3) client.delete - assert client.frozen? + assert_predicate client, :frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_new_record - client = Client.new + client = Client.new(name: "37signals") client.destroy - assert client.frozen? + assert_predicate client, :frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_record_with_associations client = Client.find(3) client.destroy - assert client.frozen? + assert_predicate client, :frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? assert_raise(RuntimeError) { client.name = "something else" } end def test_update_attribute - assert !Topic.find(1).approved? + assert_not_predicate Topic.find(1), :approved? Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? + assert_predicate Topic.find(1), :approved? Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? + assert_not_predicate Topic.find(1), :approved? Topic.find(1).update_attribute(:change_approved_before_save, true) - assert Topic.find(1).approved? + assert_predicate Topic.find(1), :approved? end def test_update_attribute_for_readonly_attribute @@ -647,8 +735,8 @@ class PersistenceTest < ActiveRecord::TestCase t = Topic.first t.update_attribute(:title, "super_title") assert_equal "super_title", t.title - assert !t.changed?, "topic should not have changed" - assert !t.title_changed?, "title should not have changed" + assert_not t.changed?, "topic should not have changed" + assert_not t.title_changed?, "title should not have changed" assert_nil t.title_change, "title change should be nil" t.reload @@ -672,14 +760,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column topic = Topic.find(1) topic.update_column("approved", true) - assert topic.approved? + assert_predicate topic, :approved? topic.reload - assert topic.approved? + assert_predicate topic, :approved? topic.update_column(:approved, false) - assert !topic.approved? + assert_not_predicate topic, :approved? topic.reload - assert !topic.approved? + assert_not_predicate topic, :approved? end def test_update_column_should_not_use_setter_method @@ -766,10 +854,10 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns topic = Topic.find(1) topic.update_columns("approved" => true, title: "Sebastian Topic") - assert topic.approved? + assert_predicate topic, :approved? assert_equal "Sebastian Topic", topic.title topic.reload - assert topic.approved? + assert_predicate topic, :approved? assert_equal "Sebastian Topic", topic.title end @@ -877,54 +965,45 @@ class PersistenceTest < ActiveRecord::TestCase def test_update topic = Topic.find(1) - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title topic.update("approved" => true, "title" => "The First Topic Updated") topic.reload - assert topic.approved? + assert_predicate topic, :approved? assert_equal "The First Topic Updated", topic.title topic.update(approved: false, title: "The First Topic") topic.reload - assert !topic.approved? - assert_equal "The First Topic", topic.title - end - - def test_update_attributes - topic = Topic.find(1) - assert !topic.approved? - assert_equal "The First Topic", topic.title - - topic.update_attributes("approved" => true, "title" => "The First Topic Updated") - topic.reload - assert topic.approved? - assert_equal "The First Topic Updated", topic.title - - topic.update_attributes(approved: false, title: "The First Topic") - topic.reload - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do - topic.update_attributes(id: 3, title: "Hm is it possible?") + topic.update(id: 3, title: "Hm is it possible?") end assert_not_nil error.cause assert_not_equal "Hm is it possible?", Topic.find(3).title - topic.update_attributes(id: 1234) + topic.update(id: 1234) assert_nothing_raised { topic.reload } assert_equal topic.title, Topic.find(1234).title end - def test_update_attributes_parameters + def test_update_attributes + topic = Topic.find(1) + assert_deprecated do + topic.update_attributes("title" => "The First Topic Updated") + end + end + + def test_update_parameters topic = Topic.find(1) assert_nothing_raised do - topic.update_attributes({}) + topic.update({}) end assert_raises(ArgumentError) do - topic.update_attributes(nil) + topic.update(nil) end end @@ -950,24 +1029,10 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_attributes! - Reply.validates_presence_of(:title) reply = Reply.find(2) - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening") - reply.reload - assert_equal "The Second Topic of the day updated", reply.title - assert_equal "Have a nice evening", reply.content - - reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day") - reply.reload - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") } - ensure - Reply.clear_validators! + assert_deprecated do + reply.update_attributes!("title" => "The Second Topic of the day updated") + end end def test_destroyed_returns_boolean @@ -1004,7 +1069,7 @@ class PersistenceTest < ActiveRecord::TestCase Topic.find(1).replies << should_be_destroyed_reply topic = Topic.destroy(1) - assert topic.destroyed? + assert_predicate topic, :destroyed? assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } @@ -1084,13 +1149,13 @@ class PersistenceTest < ActiveRecord::TestCase def test_find_via_reload post = Post.new - assert post.new_record? + assert_predicate post, :new_record? post.id = 1 post.reload assert_equal "Welcome to the weblog", post.title - assert_not post.new_record? + assert_not_predicate post, :new_record? end def test_reload_via_querycache diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 80016fc19d..4ed7469039 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -207,13 +207,13 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_serial_with_quoted_sequence_name column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function - assert column.serial? + assert_predicate column, :serial? end def test_serial_with_unquoted_sequence_name column = Topic.columns_hash[Topic.primary_key] assert_equal "nextval('topics_id_seq'::regclass)", column.default_function - assert column.serial? + assert_predicate column, :serial? end end end @@ -305,6 +305,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase test "schema dump primary key includes type and options" do schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema + assert_no_match %r{t\.index \["code"\]}, schema end if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported? @@ -430,7 +431,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) @connection.create_table(:widgets, id: @pk_type, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } assert_equal :integer, column.type - assert_not column.bigint? + assert_not_predicate column, :bigint? end test "primary key with serial/integer are automatically numbered" do @@ -449,10 +450,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) test "primary key column type with options" do @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? + assert_predicate column, :auto_increment? assert_equal :integer, column.type - assert_not column.bigint? - assert column.unsigned? + assert_not_predicate column, :bigint? + assert_predicate column, :unsigned? schema = dump_table_schema "widgets" assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema @@ -461,10 +462,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) test "bigint primary key with unsigned" do @connection.create_table(:widgets, id: :bigint, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? + assert_predicate column, :auto_increment? assert_equal :integer, column.type - assert column.bigint? - assert column.unsigned? + assert_predicate column, :bigint? + assert_predicate column, :unsigned? schema = dump_table_schema "widgets" assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index ad05f70933..69be091869 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -13,12 +13,13 @@ class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts class ShouldNotHaveExceptionsLogger < ActiveRecord::LogSubscriber - attr_reader :logger + attr_reader :logger, :events def initialize super @logger = ::Logger.new File::NULL @exception = false + @events = [] end def exception? @@ -26,6 +27,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def sql(event) + @events << event super rescue @exception = true @@ -82,7 +84,7 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off, conn end - assert !ActiveRecord::Base.connection.nil? + assert_not_predicate ActiveRecord::Base.connection, :nil? assert_cache :off middleware { @@ -265,6 +267,26 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_cache_notifications_can_be_overridden + logger = ShouldNotHaveExceptionsLogger.new + subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger + + connection = ActiveRecord::Base.connection.dup + + def connection.cache_notification_info(sql, name, binds) + super.merge(neat: true) + end + + connection.cache do + connection.select_all "select 1" + connection.select_all "select 1" + end + + assert_equal true, logger.events.last.payload[:neat] + ensure + ActiveSupport::Notifications.unsubscribe subscriber + end + def test_cache_does_not_raise_exceptions logger = ShouldNotHaveExceptionsLogger.new subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger @@ -320,6 +342,17 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_cache_is_available_when_connection_is_connected + conf = ActiveRecord::Base.configurations + + ActiveRecord::Base.configurations = {} + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + ensure + ActiveRecord::Base.configurations = conf + end + def test_cache_is_available_when_using_a_not_connected_connection skip "In-Memory DB can't test for using a not connected connection" if in_memory_db? with_temporary_connection_pool do @@ -327,7 +360,7 @@ class QueryCacheTest < ActiveRecord::TestCase conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") ActiveRecord::Base.connection_handler.establish_connection(conf) Task.connection_specification_name = "test2" - refute Task.connected? + assert_not_predicate Task, :connected? Task.cache do begin @@ -366,7 +399,7 @@ class QueryCacheTest < ActiveRecord::TestCase post = Post.first Post.transaction do - post.update_attributes(title: "rollback") + post.update(title: "rollback") assert_equal 1, Post.where(title: "rollback").to_a.count raise ActiveRecord::Rollback end @@ -379,7 +412,7 @@ class QueryCacheTest < ActiveRecord::TestCase begin Post.transaction do - post.update_attributes(title: "rollback") + post.update(title: "rollback") assert_equal 1, Post.where(title: "rollback").to_a.count raise "broken" end @@ -414,24 +447,25 @@ class QueryCacheTest < ActiveRecord::TestCase def test_query_cache_does_not_establish_connection_if_unconnected with_temporary_connection_pool do ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" }.call({}) - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" end end def test_query_cache_is_enabled_on_connections_established_after_middleware_runs with_temporary_connection_pool do ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { - assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled }.call({}) + assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled end end @@ -444,11 +478,10 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache_enabled Thread.new { - refute ActiveRecord::Base.connection_pool.query_cache_enabled - refute ActiveRecord::Base.connection.query_cache_enabled + assert_not ActiveRecord::Base.connection_pool.query_cache_enabled + assert_not ActiveRecord::Base.connection.query_cache_enabled }.join }.call({}) - end end @@ -471,14 +504,14 @@ class QueryCacheTest < ActiveRecord::TestCase def assert_cache(state, connection = ActiveRecord::Base.connection) case state when :off - assert !connection.query_cache_enabled, "cache should be off" + assert_not connection.query_cache_enabled, "cache should be off" assert connection.query_cache.empty?, "cache should be empty" when :clean assert connection.query_cache_enabled, "cache should be on" assert connection.query_cache.empty?, "cache should be empty" when :dirty assert connection.query_cache_enabled, "cache should be on" - assert !connection.query_cache.empty?, "cache should be dirty" + assert_not connection.query_cache.empty?, "cache should be dirty" else raise "unknown state" end @@ -506,19 +539,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_find assert_called(Task.connection, :clear_query_cache) do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.cache do assert Task.connection.query_cache_enabled Task.find(1) Task.uncached do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.find(1) end assert Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled end end @@ -562,7 +595,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do ActiveRecord::Base.cache do p = Post.find(1) - assert p.categories.any? + assert_predicate p.categories, :any? p.categories.delete_all end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 6534770c57..723fccc8d9 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -46,27 +46,86 @@ module ActiveRecord assert_equal t.to_s(:db), @quoter.quoted_date(t) end - def test_quoted_time_utc + def test_quoted_timestamp_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0) assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_local + def test_quoted_timestamp_local with_timezone_config default: :local do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_crazy + def test_quoted_timestamp_crazy with_timezone_config default: :asdfasdf do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end + def test_quoted_time_utc + with_timezone_config default: :utc do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getutc.to_s(:db).slice(11..-1) + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_local + with_timezone_config default: :local do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_dst_utc + with_env_tz "America/New_York" do + with_timezone_config default: :utc do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getutc.to_s(:db).slice(11..-1) + + assert_equal expected, @quoter.quoted_time(t) + end + end + end + + def test_quoted_time_dst_local + with_env_tz "America/New_York" do + with_timezone_config default: :local do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).slice(11..-1) + + assert_equal expected, @quoter.quoted_time(t) + end + end + end + + def test_quoted_time_crazy + with_timezone_config default: :asdfasdf do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + def test_quoted_datetime_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0).to_datetime diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index d1b85cb4ef..059fa76132 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -16,14 +16,14 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_cant_save_readonly_record dev = Developer.find(1) - assert !dev.readonly? + assert_not_predicate dev, :readonly? dev.readonly! - assert dev.readonly? + assert_predicate dev, :readonly? assert_nothing_raised do dev.name = "Luscious forbidden fruit." - assert !dev.save + assert_not dev.save dev.name = "Forbidden." end @@ -38,8 +38,8 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } - Developer.readonly(false).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } + Developer.readonly(false).each { |d| assert_not d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } end @@ -54,56 +54,56 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_has_many_find_readonly post = Post.find(1) - assert !post.comments.empty? - assert !post.comments.any?(&:readonly?) - assert !post.comments.to_a.any?(&:readonly?) + assert_not_empty post.comments + assert_not post.comments.any?(&:readonly?) + assert_not post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly assert people = Post.find(1).people - assert !people.any?(&:readonly?) + assert_not people.any?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id - assert !posts(:welcome).people.find(1).readonly? + assert_not_predicate posts(:welcome).people.find(1), :readonly? end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first - assert !posts(:welcome).people.first.readonly? + assert_not_predicate posts(:welcome).people.first, :readonly? end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last - assert !posts(:welcome).people.last.readonly? + assert_not_predicate posts(:welcome).people.last, :readonly? end def test_readonly_scoping Post.where("1=1").scoping do - assert !Post.find(1).readonly? - assert Post.readonly(true).find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly(true).find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end Post.joins(" ").scoping do - assert !Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end # Oracle barfs on this because the join includes unqualified and # conflicting column names unless current_adapter?(:OracleAdapter) Post.joins(", developers").scoping do - assert_not Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end end Post.readonly(true).scoping do - assert Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end end @@ -111,10 +111,10 @@ class ReadOnlyTest < ActiveRecord::TestCase developer = Developer.find(1) project = Post.find(1) - assert !developer.projects.all_as_method.first.readonly? - assert !developer.projects.all_as_scope.first.readonly? + assert_not_predicate developer.projects.all_as_method.first, :readonly? + assert_not_predicate developer.projects.all_as_scope.first, :readonly? - assert !project.comments.all_as_method.first.readonly? - assert !project.comments.all_as_scope.first.readonly? + assert_not_predicate project.comments.all_as_method.first, :readonly? + assert_not_predicate project.comments.all_as_scope.first, :readonly? end end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 6c7727ab1b..b034fe3e3b 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -36,15 +36,15 @@ module ActiveRecord # A reaper with nil time should never reap connections def test_nil_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, nil) reaper.run - assert !fp.reaped + assert_not fp.reaped end def test_some_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run @@ -79,14 +79,14 @@ module ActiveRecord end Thread.pass while conn.nil? - assert conn.in_use? + assert_predicate conn, :in_use? child.terminate while conn.in_use? Thread.pass end - assert !conn.in_use? + assert_not_predicate conn, :in_use? end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 44055e5ab6..abadafbad4 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -66,13 +66,16 @@ class ReflectionTest < ActiveRecord::TestCase def test_column_string_type_and_limit assert_equal :string, @first.column_for_attribute("title").type + assert_equal :string, @first.column_for_attribute(:title).type + assert_equal :string, @first.type_for_attribute("title").type + assert_equal :string, @first.type_for_attribute(:title).type assert_equal 250, @first.column_for_attribute("title").limit end def test_column_null_not_null subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null - assert !subscriber.column_for_attribute("nick").null + assert_not subscriber.column_for_attribute("nick").null end def test_human_name_for_column @@ -81,6 +84,9 @@ class ReflectionTest < ActiveRecord::TestCase def test_integer_columns assert_equal :integer, @first.column_for_attribute("id").type + assert_equal :integer, @first.column_for_attribute(:id).type + assert_equal :integer, @first.type_for_attribute("id").type + assert_equal :integer, @first.type_for_attribute(:id).type end def test_non_existent_columns_return_null_object @@ -89,6 +95,9 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "attribute_that_doesnt_exist", column.name assert_nil column.sql_type assert_nil column.type + + column = @first.column_for_attribute(:attribute_that_doesnt_exist) + assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column end def test_non_existent_types_are_identity_types @@ -98,6 +107,11 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal object, type.deserialize(object) assert_equal object, type.cast(object) assert_equal object, type.serialize(object) + + type = @first.type_for_attribute(:attribute_that_doesnt_exist) + assert_equal object, type.deserialize(object) + assert_equal object, type.cast(object) + assert_equal object, type.serialize(object) end def test_reflection_klass_for_nested_class_name @@ -149,7 +163,7 @@ class ReflectionTest < ActiveRecord::TestCase expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] } received = Pirate.reflect_on_all_autosave_associations - assert !received.empty? + assert_not_empty received assert_not_equal Pirate.reflect_on_all_associations.length, received.length assert_equal expected, received end @@ -254,23 +268,34 @@ class ReflectionTest < ActiveRecord::TestCase end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case - @hotel = Hotel.create! - @department = @hotel.departments.create! - @department.chefs.create!(employable: CakeDesigner.create!) - @department.chefs.create!(employable: DrinkDesigner.create!) + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create!(employable: CakeDesigner.create!) + department.chefs.create!(employable: DrinkDesigner.create!) - assert_equal 1, @hotel.cake_designers.size - assert_equal 1, @hotel.drink_designers.size - assert_equal 2, @hotel.chefs.size + assert_equal 1, hotel.cake_designers.size + assert_equal 1, hotel.cake_designers.count + assert_equal 1, hotel.drink_designers.size + assert_equal 1, hotel.drink_designers.count + assert_equal 2, hotel.chefs.size + assert_equal 2, hotel.chefs.count end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti - @hotel = Hotel.create! - @hotel.mocktail_designers << MocktailDesigner.create! + hotel = Hotel.create! + hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, hotel.mocktail_designers.size + assert_equal 1, hotel.mocktail_designers.count + assert_equal 1, hotel.chef_lists.size + assert_equal 1, hotel.chef_lists.count + + hotel.mocktail_designers = [] - assert_equal 1, @hotel.mocktail_designers.size - assert_equal 1, @hotel.mocktail_designers.count - assert_equal 1, @hotel.chef_lists.size + assert_equal 0, hotel.mocktail_designers.size + assert_equal 0, hotel.mocktail_designers.count + assert_equal 0, hotel.chef_lists.size + assert_equal 0, hotel.chef_lists.count end def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations @@ -290,12 +315,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_nested? - assert !Author.reflect_on_association(:comments).nested? - assert Author.reflect_on_association(:tags).nested? + assert_not_predicate Author.reflect_on_association(:comments), :nested? + assert_predicate Author.reflect_on_association(:tags), :nested? # Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is # a nested through association - assert Category.reflect_on_association(:post_comments).nested? + assert_predicate Category.reflect_on_association(:post_comments), :nested? end def test_association_primary_key @@ -343,36 +368,36 @@ class ReflectionTest < ActiveRecord::TestCase end def test_collection_association - assert Pirate.reflect_on_association(:birds).collection? - assert Pirate.reflect_on_association(:parrots).collection? + assert_predicate Pirate.reflect_on_association(:birds), :collection? + assert_predicate Pirate.reflect_on_association(:parrots), :collection? - assert !Pirate.reflect_on_association(:ship).collection? - assert !Ship.reflect_on_association(:pirate).collection? + assert_not_predicate Pirate.reflect_on_association(:ship), :collection? + assert_not_predicate Ship.reflect_on_association(:pirate), :collection? end def test_default_association_validation - assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? + assert_predicate ActiveRecord::Reflection.create(:has_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? + assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm), :validate? end def test_always_validate_association_if_explicit - 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? + assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm), :validate? end def test_validate_association_if_autosave - 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? + assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm), :validate? end def test_never_validate_association_if_explicit - 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? + assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm), :validate? end def test_foreign_key diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 2696d1bb00..3f3d41980c 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -54,4 +54,32 @@ module ActiveRecord Comment.all end end + + class QueryingMethodsDelegationTest < ActiveRecord::TestCase + QUERYING_METHODS = [ + :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, + :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, + :first_or_create, :first_or_create!, :first_or_initialize, + :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, + :find_by, :find_by!, + :destroy_all, :delete_all, :update_all, + :find_each, :find_in_batches, :in_batches, + :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, + :count, :average, :minimum, :maximum, :sum, :calculate, + :pluck, :pick, :ids, + ] + + def test_delegate_querying_methods + klass = Class.new(ActiveRecord::Base) do + self.table_name = "posts" + end + + QUERYING_METHODS.each do |method| + assert_respond_to klass.all, method + assert_respond_to klass, method + end + end + end end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index b68b3723f6..f53ef1fe35 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -58,7 +58,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked? + assert_predicate devs, :locked? end def test_relation_merging_with_preload @@ -72,6 +72,16 @@ class RelationMergingTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_left_outer_joins + comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day")) + + assert_equal 1, comments.count + end + + def test_relation_merging_with_skip_query_cache + assert_equal Post.all.merge(Post.all.skip_query_cache!).skip_query_cache_value, true + end + def test_relation_merging_with_association assert_queries(2) do # one for loading post, and another one merged query post = Post.where(body: "Such a lovely day").first @@ -107,9 +117,9 @@ class RelationMergingTest < ActiveRecord::TestCase def test_merging_with_from_clause relation = Post.all - assert relation.from_clause.empty? + assert_empty relation.from_clause relation = relation.merge(Post.from("posts")) - refute relation.from_clause.empty? + assert_not_empty relation.from_clause end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index ad3700b73a..f82ecd4449 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord test "#order! with symbol prepends the table name" do assert relation.order!(:name).equal?(relation) node = relation.order_values.first - assert node.ascending? + assert_predicate node, :ascending? assert_equal :name, node.expr.name assert_equal "posts", node.expr.relation.name end @@ -88,7 +88,7 @@ module ActiveRecord assert relation.reorder!(:name).equal?(relation) node = relation.order_values.first - assert node.ascending? + assert_predicate node, :ascending? assert_equal :name, node.expr.name assert_equal "posts", node.expr.relation.name end @@ -137,9 +137,14 @@ module ActiveRecord assert relation.skip_query_cache_value end + test "skip_preloading!" do + relation.skip_preloading! + assert relation.skip_preloading_value + end + private def relation - @relation ||= Relation.new(FakeKlass, Post.arel_table, Post.predicate_builder) + @relation ||= Relation.new(FakeKlass) end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 7e418f9c7d..065819e0f1 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -126,5 +126,12 @@ module ActiveRecord expected = Author.find(1).posts + Post.where(title: "I don't have any comments") assert_equal expected.sort_by(&:id), actual.sort_by(&:id) end + + def test_or_with_scope_on_association + author = Author.first + assert_nothing_raised do + author.top_posts.or(author.other_top_posts) + end + end end end diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb new file mode 100644 index 0000000000..dec8a6925d --- /dev/null +++ b/activerecord/test/cases/relation/select_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +module ActiveRecord + class SelectTest < ActiveRecord::TestCase + fixtures :posts + + def test_select_with_nil_argument + expected = Post.select(:title).to_sql + assert_equal expected, Post.select(nil).select(:title).to_sql + end + end +end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index e5eb159d36..8703d238a0 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -75,8 +75,8 @@ class ActiveRecord::Relation end test "a clause knows if it is empty" do - assert WhereClause.empty.empty? - assert_not WhereClause.new(["anything"]).empty? + assert_empty WhereClause.empty + assert_not_empty WhereClause.new(["anything"]) end test "invert cannot handle nil" do diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index dbf3389774..fbeb617b29 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -5,25 +5,26 @@ require "models/post" require "models/comment" require "models/author" require "models/rating" +require "models/categorization" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses, :ratings + fixtures :posts, :comments, :authors, :author_addresses, :ratings, :categorizations def test_construction - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass, table: :b) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table - assert !relation.loaded, "relation is not loaded" + assert_not relation.loaded, "relation is not loaded" end def test_responds_to_model_and_returns_klass - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -33,7 +34,7 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) Relation::MULTI_VALUE_METHODS.each do |method| values = relation.send("#{method}_values") assert_equal [], values, method.to_s @@ -42,29 +43,29 @@ module ActiveRecord end def test_extensions - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal({}, relation.where_values_hash) end def test_has_values - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.where!(id: 10) assert_equal({ "id" => 10 }, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.or(right) @@ -73,18 +74,18 @@ module ActiveRecord end def test_scope_for_create - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.create_with_value = { hello: "world" } assert_equal({ "hello" => "world" }, relation.scope_for_create) end def test_create_with_value_with_wheres - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) assert_equal({}, relation.scope_for_create) relation.where!(id: 10) @@ -95,11 +96,11 @@ module ActiveRecord end def test_empty_scope - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - assert relation.empty_scope? + relation = Relation.new(Post) + assert_predicate relation, :empty_scope? relation.merge!(relation) - assert relation.empty_scope? + assert_predicate relation, :empty_scope? end def test_bad_constants_raise_errors @@ -109,31 +110,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new(FakeKlass, :b, nil) - assert !relation.eager_loading? + relation = Relation.new(FakeKlass) + assert_not_predicate relation, :eager_loading? end def test_eager_load_values - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation.eager_load! :b - assert relation.eager_loading? + assert_predicate relation, :eager_loading? end def test_references_values - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ["foo", "omg", "lol"], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation = relation.references(:foo).references(:foo) assert_equal ["foo"], relation.references_values end test "merging a hash into a relation" do - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation = relation.merge where: { name: :lol }, readonly: true assert_equal({ "name" => :lol }, relation.where_clause.to_h) @@ -141,7 +142,7 @@ module ActiveRecord end test "merging an empty hash into a relation" do - assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause + assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause end test "merging a hash with unknown keys raises" do @@ -149,7 +150,7 @@ module ActiveRecord end test "merging nil or false raises" do - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) e = assert_raises(ArgumentError) do relation = relation.merge nil @@ -165,7 +166,7 @@ module ActiveRecord end test "#values returns a dup of the values" do - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo) + relation = Relation.new(Post).where!(name: :foo) values = relation.values values[:where] = nil @@ -173,7 +174,7 @@ module ActiveRecord end test "relations can be created with a values hash" do - relation = Relation.new(FakeKlass, :b, nil, select: [:foo]) + relation = Relation.new(FakeKlass, values: { select: [:foo] }) assert_equal [:foo], relation.select_values end @@ -185,13 +186,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b, nil) + relation = Relation.new(klass) relation.merge!(where: ["foo = ?", "bar"]) assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause end def test_merging_readonly_false - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value @@ -223,6 +224,30 @@ module ActiveRecord assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size end + def test_relation_merging_with_merged_symbol_joins_is_aliased + categorizations_with_authors = Categorization.joins(:author) + queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a } + + nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size } + assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query" + + # using `\W` as the column separator + assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Author.quoted_table_name}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query" + end + + def test_relation_with_merged_joins_aliased_works + categorizations_with_authors = Categorization.joins(:author) + posts_with_joins_and_merges = Post.joins(:author, :categorizations) + .merge(Author.select(:id)).merge(categorizations_with_authors) + + author_with_posts = Author.joins(:posts).ids + categorizations_with_author = Categorization.joins(:author).ids + posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).ids + + assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.count + assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.to_a.size + end + def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent post = Post.create!(title: "haha", body: "huhu") comment = post.comments.create!(body: "hu") @@ -235,17 +260,17 @@ module ActiveRecord def test_merge_raises_with_invalid_argument assert_raises ArgumentError do - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation.merge(true) end end def test_respond_to_for_non_selected_element post = Post.select(:title).first - assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" + assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception" silence_warnings { post = Post.select("'title' as post_title").first } - assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" + assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception" end def test_select_quotes_when_using_from_clause @@ -315,6 +340,14 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + def test_skip_preloading_after_arel_has_been_generated + assert_nothing_raised do + relation = Comment.all + relation.arel + relation.skip_preloading! + end + end + private def skip_if_sqlite3_version_includes_quoting_bug diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 7785f8c99b..5412ab5def 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -9,6 +9,7 @@ require "models/comment" require "models/author" require "models/entrant" require "models/developer" +require "models/person" require "models/computer" require "models/reply" require "models/company" @@ -22,13 +23,16 @@ require "models/reader" require "models/category" require "models/categorization" require "models/edge" +require "models/subscriber" class RelationTest < ActiveRecord::TestCase - fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans + fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans class TopicWithCallbacks < ActiveRecord::Base self.table_name = :topics + cattr_accessor :topic_count before_update { |topic| topic.author_name = "David" if topic.author_name.blank? } + after_update { |topic| topic.class.topic_count = topic.class.count } end def test_do_not_double_quote_string_id @@ -56,7 +60,7 @@ class RelationTest < ActiveRecord::TestCase def test_dynamic_finder x = Post.where("author_id = ?", 1) - assert x.klass.respond_to?(:find_by_id), "@klass should handle dynamic finders" + assert_respond_to x.klass, :find_by_id end def test_multivalue_where @@ -98,7 +102,7 @@ class RelationTest < ActiveRecord::TestCase 2.times { assert_equal 5, topics.to_a.size } end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_scoped_first @@ -108,7 +112,7 @@ class RelationTest < ActiveRecord::TestCase 2.times { assert_equal "The First Topic", topics.first.title } end - assert ! topics.loaded? + assert_not_predicate topics, :loaded? end def test_loaded_first @@ -119,7 +123,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal "The First Topic", topics.first.title end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_loaded_first_with_limit @@ -131,7 +135,7 @@ class RelationTest < ActiveRecord::TestCase "The Second Topic of the day"], topics.first(2).map(&:title) end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_first_get_more_than_available @@ -152,14 +156,14 @@ class RelationTest < ActiveRecord::TestCase 2.times { topics.to_a } end - assert topics.loaded? + assert_predicate topics, :loaded? original_size = topics.to_a.size Topic.create! title: "fake" assert_queries(1) { topics.reload } assert_equal original_size + 1, topics.size - assert topics.loaded? + assert_predicate topics, :loaded? end def test_finding_with_subquery @@ -501,16 +505,16 @@ class RelationTest < ActiveRecord::TestCase relation = Topic.all ["find_by_title", "find_by_title_and_author_name"].each do |method| - assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" + assert_respond_to relation, method end end def test_respond_to_class_methods_and_scopes - assert Topic.all.respond_to?(:by_lifo) + assert_respond_to Topic.all, :by_lifo end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } Developer.all.readonly.each { |d| assert d.readonly? } end @@ -601,7 +605,7 @@ class RelationTest < ActiveRecord::TestCase reader = Reader.create! post_id: post.id, person_id: 1 comment = Comment.create! post_id: post.id, body: "body" - assert !comment.respond_to?(:readers) + assert_not_respond_to comment, :readers post_rel = Post.preload(:readers).joins(:readers).where(title: "Uhuu") result_comment = Comment.joins(:post).merge(post_rel).to_a.first @@ -722,7 +726,7 @@ class RelationTest < ActiveRecord::TestCase def test_find_in_empty_array authors = Author.all.where(id: []) - assert authors.to_a.blank? + assert_predicate authors.to_a, :blank? end def test_where_with_ar_object @@ -863,19 +867,19 @@ class RelationTest < ActiveRecord::TestCase # Force load assert_equal [authors(:david)], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? assert_difference("Author.count", -1) { davids.destroy_all } assert_equal [], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? end def test_delete_all davids = Author.where(name: "David") assert_difference("Author.count", -1) { davids.delete_all } - assert ! davids.loaded? + assert_not_predicate davids, :loaded? end def test_delete_all_loaded @@ -883,12 +887,12 @@ class RelationTest < ActiveRecord::TestCase # Force load assert_equal [authors(:david)], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? assert_difference("Author.count", -1) { davids.delete_all } assert_equal [], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? end def test_delete_all_with_unpermitted_relation_raises_error @@ -902,9 +906,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.count(:all) assert_equal 11, posts.size - assert posts.any? - assert posts.many? - assert_not posts.empty? + assert_predicate posts, :any? + assert_predicate posts, :many? + assert_not_empty posts end def test_select_takes_a_variable_list_of_args @@ -950,7 +954,7 @@ class RelationTest < ActiveRecord::TestCase 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? + assert_empty author.posts.where(author_id: another_author.id) end def test_count_with_distinct @@ -969,6 +973,18 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 8, posts.load.size } end + def test_size_with_eager_loading_and_custom_order + posts = Post.includes(:comments).order("comments.id") + assert_queries(1) { assert_equal 11, posts.size } + assert_queries(1) { assert_equal 11, posts.load.size } + end + + def test_size_with_eager_loading_and_custom_order_and_distinct + posts = Post.includes(:comments).order("comments.id").distinct + assert_queries(1) { assert_equal 11, posts.size } + assert_queries(1) { assert_equal 11, posts.load.size } + end + def test_update_all_with_scope tag = Tag.first Post.tagged_with(tag.id).update_all title: "rofl" @@ -1000,7 +1016,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.all assert_queries(1) { assert_equal 11, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1011,7 +1027,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(10) assert_queries(1) { assert_equal 10, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1022,7 +1038,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(0) assert_no_queries { assert_equal 0, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? posts.load # force load assert_no_queries { assert_equal 0, posts.size } @@ -1032,7 +1048,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(0) assert_no_queries { assert_equal true, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? end def test_count_complex_chained_relations @@ -1046,11 +1062,11 @@ class RelationTest < ActiveRecord::TestCase posts = Post.all assert_queries(1) { assert_equal false, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? no_posts = posts.where(title: "") assert_queries(1) { assert_equal true, no_posts.empty? } - assert ! no_posts.loaded? + assert_not_predicate no_posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1061,11 +1077,11 @@ class RelationTest < ActiveRecord::TestCase posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0") assert_queries(1) { assert_equal false, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? no_posts = posts.where(title: "") assert_queries(1) { assert_equal true, no_posts.empty? } - assert ! no_posts.loaded? + assert_not_predicate no_posts, :loaded? end def test_any @@ -1081,13 +1097,13 @@ class RelationTest < ActiveRecord::TestCase assert_queries(3) do assert posts.any? # Uses COUNT() - assert ! posts.where(id: nil).any? + assert_not_predicate posts.where(id: nil), :any? assert posts.any? { |p| p.id > 0 } - assert ! posts.any? { |p| p.id <= 0 } + assert_not posts.any? { |p| p.id <= 0 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_many @@ -1096,49 +1112,49 @@ class RelationTest < ActiveRecord::TestCase assert_queries(2) do assert posts.many? # Uses COUNT() assert posts.many? { |p| p.id > 0 } - assert ! posts.many? { |p| p.id < 2 } + assert_not posts.many? { |p| p.id < 2 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_many_with_limits posts = Post.all - assert posts.many? - assert ! posts.limit(1).many? + assert_predicate posts, :many? + assert_not_predicate posts.limit(1), :many? end def test_none? posts = Post.all assert_queries(1) do - assert ! posts.none? # Uses COUNT() + assert_not posts.none? # Uses COUNT() end - assert ! posts.loaded? + assert_not_predicate posts, :loaded? assert_queries(1) do assert posts.none? { |p| p.id < 0 } - assert ! posts.none? { |p| p.id == 1 } + assert_not posts.none? { |p| p.id == 1 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_one posts = Post.all assert_queries(1) do - assert ! posts.one? # Uses COUNT() + assert_not posts.one? # Uses COUNT() end - assert ! posts.loaded? + assert_not_predicate posts, :loaded? assert_queries(1) do - assert ! posts.one? { |p| p.id < 3 } + assert_not posts.one? { |p| p.id < 3 } assert posts.one? { |p| p.id == 1 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_to_a_should_dup_target @@ -1171,10 +1187,10 @@ class RelationTest < ActiveRecord::TestCase sparrow = birds.create assert_kind_of Bird, sparrow - assert !sparrow.persisted? + assert_not_predicate sparrow, :persisted? hen = birds.where(name: "hen").create - assert hen.persisted? + assert_predicate hen, :persisted? assert_equal "hen", hen.name end @@ -1185,7 +1201,7 @@ class RelationTest < ActiveRecord::TestCase hen = birds.where(name: "hen").create! assert_kind_of Bird, hen - assert hen.persisted? + assert_predicate hen, :persisted? assert_equal "hen", hen.name end @@ -1201,27 +1217,27 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_create parrot = Bird.where(color: "green").first_or_create(name: "parrot") assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "parrot", parrot.name assert_equal "green", parrot.color same_parrot = Bird.where(color: "green").first_or_create(name: "parakeet") assert_kind_of Bird, same_parrot - assert same_parrot.persisted? + assert_predicate same_parrot, :persisted? assert_equal parrot, same_parrot end def test_first_or_create_with_no_parameters parrot = Bird.where(color: "green").first_or_create assert_kind_of Bird, parrot - assert !parrot.persisted? + assert_not_predicate parrot, :persisted? assert_equal "green", parrot.color end def test_first_or_create_with_block parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "green", parrot.color assert_equal "parrot", parrot.name @@ -1242,13 +1258,13 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_create_bang_with_valid_options parrot = Bird.where(color: "green").first_or_create!(name: "parrot") assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "parrot", parrot.name assert_equal "green", parrot.color same_parrot = Bird.where(color: "green").first_or_create!(name: "parakeet") assert_kind_of Bird, same_parrot - assert same_parrot.persisted? + assert_predicate same_parrot, :persisted? assert_equal parrot, same_parrot end @@ -1263,7 +1279,7 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_create_bang_with_valid_block parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "green", parrot.color assert_equal "parrot", parrot.name @@ -1294,9 +1310,9 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_initialize parrot = Bird.where(color: "green").first_or_initialize(name: "parrot") assert_kind_of Bird, parrot - assert !parrot.persisted? - assert parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "parrot", parrot.name assert_equal "green", parrot.color end @@ -1304,18 +1320,18 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_initialize_with_no_parameters parrot = Bird.where(color: "green").first_or_initialize assert_kind_of Bird, parrot - assert !parrot.persisted? - assert !parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_not_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "green", parrot.color end def test_first_or_initialize_with_block parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert !parrot.persisted? - assert parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "green", parrot.color assert_equal "parrot", parrot.name end @@ -1324,7 +1340,7 @@ class RelationTest < ActiveRecord::TestCase assert_nil Bird.find_by(name: "bob") bird = Bird.find_or_create_by(name: "bob") - assert bird.persisted? + assert_predicate bird, :persisted? assert_equal bird, Bird.find_or_create_by(name: "bob") end @@ -1333,7 +1349,7 @@ class RelationTest < ActiveRecord::TestCase assert_nil Bird.find_by(name: "bob") bird = Bird.create_with(color: "green").find_or_create_by(name: "bob") - assert bird.persisted? + assert_predicate bird, :persisted? assert_equal "green", bird.color assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob") @@ -1343,11 +1359,39 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") } end + def test_create_or_find_by + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") + end + + def test_create_or_find_by_with_non_unique_attributes + Subscriber.create!(nick: "bob", name: "the builder") + + assert_raises(ActiveRecord::RecordNotFound) do + Subscriber.create_or_find_by(nick: "bob", name: "the cat") + end + end + + def test_create_or_find_by_within_transaction + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + Subscriber.transaction do + assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") + end + end + def test_find_or_initialize_by assert_nil Bird.find_by(name: "bob") bird = Bird.find_or_initialize_by(name: "bob") - assert bird.new_record? + assert_predicate bird, :new_record? bird.save! assert_equal bird, Bird.find_or_initialize_by(name: "bob") @@ -1484,12 +1528,58 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end + def test_touch_all_updates_records_timestamps + david = developers(:david) + david_previously_updated_at = david.updated_at + jamis = developers(:jamis) + jamis_previously_updated_at = jamis.updated_at + Developer.where(name: "David").touch_all + + assert_not_equal david_previously_updated_at, david.reload.updated_at + assert_equal jamis_previously_updated_at, jamis.reload.updated_at + end + + def test_touch_all_with_custom_timestamp + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + Developer.where(name: "David").touch_all(:created_at) + developer = developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + end + + def test_touch_all_with_given_time + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + Developer.where(name: "David").touch_all(:created_at, time: new_time) + developer = developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + assert_equal new_time, developer.created_at + assert_equal new_time, developer.updated_at + end + + def test_touch_all_updates_locking_column + person = people(:david) + + assert_difference -> { person.reload.lock_version }, +1 do + Person.where(first_name: "David").touch_all + end + end + def test_update_on_relation topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) topics.update(title: "adequaterecord") + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + assert_equal "adequaterecord", topic1.reload.title assert_equal "adequaterecord", topic2.reload.title # Testing that the before_update callbacks have run @@ -1522,10 +1612,10 @@ class RelationTest < ActiveRecord::TestCase def test_doesnt_add_having_values_if_options_are_blank scope = Post.having("") - assert scope.having_clause.empty? + assert_empty scope.having_clause scope = Post.having([]) - assert scope.having_clause.empty? + assert_empty scope.having_clause end def test_having_with_binds_for_both_where_and_having @@ -1551,13 +1641,13 @@ class RelationTest < ActiveRecord::TestCase def test_references_triggers_eager_loading scope = Post.includes(:comments) - assert !scope.eager_loading? - assert scope.references(:comments).eager_loading? + assert_not_predicate scope, :eager_loading? + assert_predicate scope.references(:comments), :eager_loading? end def test_references_doesnt_trigger_eager_loading_if_reference_not_included scope = Post.references(:comments) - assert !scope.eager_loading? + assert_not_predicate scope, :eager_loading? end def test_automatically_added_where_references @@ -1655,7 +1745,7 @@ class RelationTest < ActiveRecord::TestCase # checking if there are topics is used before you actually display them, # thus it shouldn't invoke an extra count query. assert_no_queries { assert topics.present? } - assert_no_queries { assert !topics.blank? } + assert_no_queries { assert_not topics.blank? } # shows count of topics and loops after loading the query should not trigger extra queries either. assert_no_queries { topics.size } @@ -1665,7 +1755,7 @@ class RelationTest < ActiveRecord::TestCase # count always trigger the COUNT query. assert_queries(1) { topics.count } - assert topics.loaded? + assert_predicate topics, :loaded? end test "find_by with hash conditions returns the first matching record" do @@ -1840,7 +1930,7 @@ class RelationTest < ActiveRecord::TestCase test "delegations do not leak to other classes" do Topic.all.by_lifo assert Topic.all.class.method_defined?(:by_lifo) - assert !Post.all.respond_to?(:by_lifo) + assert_not_respond_to Post.all, :by_lifo end def test_unscope_with_subquery @@ -1865,7 +1955,7 @@ class RelationTest < ActiveRecord::TestCase def test_locked_should_not_build_arel posts = Post.locked - assert posts.locked? + assert_predicate posts, :locked? assert_nothing_raised { posts.lock!(false) } end @@ -1934,6 +2024,10 @@ class RelationTest < ActiveRecord::TestCase table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - ActiveRecord::Relation.create(Post, table_alias, predicate_builder) + ActiveRecord::Relation.create( + Post, + table: table_alias, + predicate_builder: predicate_builder + ) end end diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb index 4f8ca392b9..e32605fd11 100644 --- a/activerecord/test/cases/reserved_word_test.rb +++ b/activerecord/test/cases/reserved_word_test.rb @@ -116,7 +116,7 @@ class ReservedWordTest < ActiveRecord::TestCase end def test_activerecord_introspection - assert Group.table_exists? + assert_predicate Group, :table_exists? assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index db52c108ac..68fcafb682 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -12,6 +12,11 @@ module ActiveRecord ]) end + test "includes_column?" do + assert result.includes_column?("col_1") + assert_not result.includes_column?("foo") + end + test "length" do assert_equal 3, result.length end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 1b0605e369..778cf86ac3 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/binary" require "models/author" require "models/post" +require "models/customer" class SanitizeTest < ActiveRecord::TestCase def setup @@ -167,6 +168,12 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end + def test_deprecated_expand_hash_conditions_for_aggregates + assert_deprecated do + assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50))) + end + end + private def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index a612ce9bb2..75b68b521c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -37,24 +37,6 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.delete_all end - if current_adapter?(:SQLite3Adapter) - %w{3.7.8 3.7.11 3.7.12}.each do |version_string| - test "dumps schema version for sqlite version #{version_string}" do - version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new(version_string) - ActiveRecord::Base.connection.stubs(:sqlite_version).returns(version) - - versions = %w{ 20100101010101 20100201010101 20100301010101 } - versions.reverse_each do |v| - ActiveRecord::SchemaMigration.create!(version: v) - end - - schema_info = ActiveRecord::Base.connection.dump_schema_information - assert_match(/20100201010101.*20100301010101/m, schema_info) - ActiveRecord::SchemaMigration.delete_all - end - end - end - def test_schema_dump output = standard_dump assert_match %r{create_table "accounts"}, output @@ -192,7 +174,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_partial_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + if ActiveRecord::Base.connection.supports_partial_index? assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition @@ -298,7 +280,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_expression_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition end def test_schema_dump_interval_type @@ -315,29 +297,33 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(["hstore"]) - output = perform_schema_dump - assert_match "# These are extensions that must be enabled", output - assert_match %r{enable_extension "hstore"}, output + connection.stub(:extensions, ["hstore"]) do + output = perform_schema_dump + assert_match "# These are extensions that must be enabled", output + assert_match %r{enable_extension "hstore"}, output + end - connection.stubs(:extensions).returns([]) - output = perform_schema_dump - assert_no_match "# These are extensions that must be enabled", output - assert_no_match %r{enable_extension}, output + connection.stub(:extensions, []) do + output = perform_schema_dump + assert_no_match "# These are extensions that must be enabled", output + assert_no_match %r{enable_extension}, output + end end def test_schema_dump_includes_extensions_in_alphabetic_order connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(["hstore", "uuid-ossp", "xml2"]) - output = perform_schema_dump - enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten - assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + connection.stub(:extensions, ["hstore", "uuid-ossp", "xml2"]) do + output = perform_schema_dump + enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten + assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + end - connection.stubs(:extensions).returns(["uuid-ossp", "xml2", "hstore"]) - output = perform_schema_dump - enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten - assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + connection.stub(:extensions, ["uuid-ossp", "xml2", "hstore"]) do + output = perform_schema_dump + enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten + assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + end end end @@ -469,7 +455,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string assert_match %r{create_table "omg_cats"}, output - refute_match %r{create_table "cats"}, output + assert_no_match %r{create_table "cats"}, output ensure migration.migrate(:down) ActiveRecord::Base.table_name_prefix = original_table_name_prefix diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index fdfeabaa3b..e3a34aa50d 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -193,7 +193,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_order_to_unscope_reordering scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order) - assert !/order/i.match?(scope.to_sql) + assert_no_match(/order/i, scope.to_sql) end def test_unscope_reverse_order @@ -224,6 +224,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_left_outer_joins + expected = Developer.all.collect(&:name) + received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name) + assert_equal expected, received + end + + def test_unscope_left_joins + expected = Developer.all.collect(&:name) + received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name) + assert_equal expected, received + end + def test_unscope_includes expected = Developer.all.collect(&:name) received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name) @@ -290,8 +302,8 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_merging merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where)) - assert merged.where_clause.empty? - assert !merged.where(name: "Jon").where_clause.empty? + assert_empty merged.where_clause + assert_not_empty merged.where(name: "Jon").where_clause end def test_order_in_default_scope_should_not_prevail diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 17d3f27bb1..4214f347fb 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -13,7 +13,7 @@ class NamedScopingTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base assert_equal Topic.all.to_a, Topic.base.to_a @@ -40,7 +40,7 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base.to_a assert_equal Topic.first, Topic.base.first @@ -65,13 +65,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy - assert Topic.approved.respond_to?(:limit) - assert Topic.approved.respond_to?(:count) - assert Topic.approved.respond_to?(:length) + assert_respond_to Topic.approved, :limit + assert_respond_to Topic.approved, :count + assert_respond_to Topic.approved, :length end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.all.merge!(where: { approved: true }).to_a.empty? + assert_not_empty Topic.all.merge!(where: { approved: true }).to_a assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved assert_equal Topic.where(approved: true).count, Topic.approved.count @@ -86,8 +86,8 @@ class NamedScopingTest < ActiveRecord::TestCase def test_scopes_are_composable assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved) assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied) - assert !(approved == replied) - assert !(approved & replied).empty? + assert_not (approved == replied) + assert_not_empty (approved & replied) assert_equal approved & replied, Topic.approved.replied end @@ -115,7 +115,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_has_many_associations_have_access_to_scopes assert_not_equal Post.containing_the_letter_a, authors(:david).posts - assert !Post.containing_the_letter_a.empty? + assert_not_empty Post.containing_the_letter_a expected = authors(:david).posts & Post.containing_the_letter_a assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id) @@ -128,15 +128,15 @@ class NamedScopingTest < ActiveRecord::TestCase def test_has_many_through_associations_have_access_to_scopes assert_not_equal Comment.containing_the_letter_e, authors(:david).comments - assert !Comment.containing_the_letter_e.empty? + assert_not_empty Comment.containing_the_letter_e expected = authors(:david).comments & Comment.containing_the_letter_e assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id) end def test_scopes_honor_current_scopes_from_when_defined - assert !Post.ranked_by_comments.limit_by(5).empty? - assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty? + assert_not_empty Post.ranked_by_comments.limit_by(5) + assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5) assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5) assert_not_equal Post.top(5), authors(:david).posts.top(5) # Oracle sometimes sorts differently if WHERE condition is changed @@ -168,14 +168,14 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_active_records_have_scope_named__all__ - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base end def test_active_records_have_scope_named__scoped__ scope = Topic.where("content LIKE '%Have%'") - assert !scope.empty? + assert_not_empty scope assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'") end @@ -228,9 +228,9 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_model_class_should_respond_to_any - assert Topic.any? + assert_predicate Topic, :any? Topic.delete_all - assert !Topic.any? + assert_not_predicate Topic, :any? end def test_many_should_not_load_results @@ -259,22 +259,22 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_return_false_if_none_or_one topics = Topic.base.where(id: 0) - assert !topics.many? + assert_not_predicate topics, :many? topics = Topic.base.where(id: 1) - assert !topics.many? + assert_not_predicate topics, :many? end def test_many_should_return_true_if_more_than_one - assert Topic.base.many? + assert_predicate Topic.base, :many? end def test_model_class_should_respond_to_many Topic.delete_all - assert !Topic.many? + assert_not_predicate Topic, :many? Topic.create! - assert !Topic.many? + assert_not_predicate Topic, :many? Topic.create! - assert Topic.many? + assert_predicate Topic, :many? end def test_should_build_on_top_of_scope @@ -303,6 +303,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "lifo", topic.author_name end + def test_deprecated_delegating_private_method + assert_deprecated do + scope = Topic.all.by_private_lifo + assert_not scope.instance_variable_get(:@delegate_to_klass) + end + end + def test_reserved_scope_names klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" @@ -423,16 +430,16 @@ class NamedScopingTest < ActiveRecord::TestCase def test_chaining_applies_last_conditions_when_creating post = Topic.rejected.new - assert !post.approved? + assert_not_predicate post, :approved? post = Topic.rejected.approved.new - assert post.approved? + assert_predicate post, :approved? post = Topic.approved.rejected.new - assert !post.approved? + assert_not_predicate post, :approved? post = Topic.approved.rejected.approved.new - assert post.approved? + assert_predicate post, :approved? end def test_chaining_combines_conditions_when_searching @@ -481,8 +488,9 @@ class NamedScopingTest < ActiveRecord::TestCase [:public_method, :protected_method, :private_method].each do |reserved_method| assert Topic.respond_to?(reserved_method, true) - ActiveRecord::Base.logger.expects(:warn) - silence_warnings { Topic.scope(reserved_method, -> {}) } + assert_called(ActiveRecord::Base.logger, :warn) do + silence_warnings { Topic.scope(reserved_method, -> {}) } + end end end @@ -498,7 +506,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_index_on_scope approved = Topic.approved.order("id ASC") assert_equal topics(:second), approved[0] - assert approved.loaded? + assert_predicate approved, :loaded? end def test_nested_scopes_queries_size @@ -578,16 +586,16 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_model_class_should_respond_to_none - assert !Topic.none? + assert_not_predicate Topic, :none? Topic.delete_all - assert Topic.none? + assert_predicate Topic, :none? end def test_model_class_should_respond_to_one - assert !Topic.one? + assert_not_predicate Topic, :one? Topic.delete_all - assert !Topic.one? + assert_not_predicate Topic, :one? Topic.create! - assert Topic.one? + assert_predicate Topic, :one? end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 116f8e83aa..544adc9b39 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -105,7 +105,7 @@ class RelationScopingTest < ActiveRecord::TestCase Developer.select("id, name").scoping do developer = Developer.where("name = 'David'").first assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) + assert_not developer.has_attribute?(:salary) end end @@ -213,21 +213,21 @@ class RelationScopingTest < ActiveRecord::TestCase def test_current_scope_does_not_pollute_sibling_subclasses Comment.none.scoping do - assert_not SpecialComment.all.any? - assert_not VerySpecialComment.all.any? - assert_not SubSpecialComment.all.any? + assert_not_predicate SpecialComment.all, :any? + assert_not_predicate VerySpecialComment.all, :any? + assert_not_predicate SubSpecialComment.all, :any? end SpecialComment.none.scoping do - assert Comment.all.any? - assert VerySpecialComment.all.any? - assert_not SubSpecialComment.all.any? + assert_predicate Comment.all, :any? + assert_predicate VerySpecialComment.all, :any? + assert_not_predicate SubSpecialComment.all, :any? end SubSpecialComment.none.scoping do - assert Comment.all.any? - assert VerySpecialComment.all.any? - assert SpecialComment.all.any? + assert_predicate Comment.all, :any? + assert_predicate VerySpecialComment.all, :any? + assert_predicate SpecialComment.all, :any? end end @@ -254,6 +254,11 @@ class RelationScopingTest < ActiveRecord::TestCase end end + def test_scoping_works_in_the_scope_block + expected = SpecialPostWithDefaultScope.unscoped.to_a + assert_equal expected, SpecialPostWithDefaultScope.unscoped_all + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) @@ -334,7 +339,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase def test_nested_exclusive_scope_for_create comment = Comment.create_with(body: "Hey guys, nested scopes are broken. Please fix!").scoping do Comment.unscoped.create_with(post_id: 1).scoping do - assert Comment.new.body.blank? + assert_predicate Comment.new.body, :blank? Comment.create body: "Hey guys" end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 2d829ad4ba..932780bfef 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -67,8 +67,8 @@ class SerializationTest < ActiveRecord::TestCase klazz.include_root_in_json = false assert ActiveRecord::Base.include_root_in_json - assert !klazz.include_root_in_json - assert !klazz.new.include_root_in_json + assert_not klazz.include_root_in_json + assert_not klazz.new.include_root_in_json ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 32dafbd458..4cd4515c3b 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -159,6 +159,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(settings, Topic.find(topic.id).content) end + def test_where_by_serialized_attribute_with_array + settings = [ "color" => "green" ] + Topic.serialize(:content, Array) + topic = Topic.create!(content: settings) + assert_equal topic, Topic.where(content: settings).take + end + def test_where_by_serialized_attribute_with_hash settings = { "color" => "green" } Topic.serialize(:content, Hash) @@ -166,6 +173,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal topic, Topic.where(content: settings).take end + def test_where_by_serialized_attribute_with_hash_in_array + settings = { "color" => "green" } + Topic.serialize(:content, Hash) + topic = Topic.create!(content: settings) + assert_equal topic, Topic.where(content: [settings]).take + end + def test_serialized_default_class Topic.serialize(:content, Hash) topic = Topic.new @@ -279,7 +293,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.new(content: nil) - assert_not topic.content_changed? + assert_not_predicate topic, :content_changed? end def test_classes_without_no_arg_constructors_are_not_supported @@ -349,7 +363,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = model.create!(foo: "bar") topic.foo - refute topic.changed? + assert_not_predicate topic, :changed? end def test_serialized_attribute_works_under_concurrent_initial_access diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index ad6cd198e2..e3c12f68fd 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -102,7 +102,7 @@ module ActiveRecord Book.find_by(name: "my other book") end - refute_equal book, other_book + assert_not_equal book, other_book end def test_find_by_does_not_use_statement_cache_if_table_name_is_changed diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index ebf4016960..4457cfbd37 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -8,7 +8,12 @@ class StoreTest < ActiveRecord::TestCase fixtures :'admin/users' setup do - @john = Admin::User.create!(name: "John Doe", color: "black", remember_login: true, height: "tall", is_a_good_guy: true) + @john = Admin::User.create!( + name: "John Doe", color: "black", remember_login: true, + height: "tall", is_a_good_guy: true, + parent_name: "Quinn", partner_name: "Dallas", + partner_birthday: "1997-11-1" + ) end test "reading store attributes through accessors" do @@ -24,6 +29,21 @@ class StoreTest < ActiveRecord::TestCase assert_equal "37signals.com", @john.homepage end + test "reading store attributes through accessors with prefix" do + assert_equal "Quinn", @john.parent_name + assert_nil @john.parent_birthday + assert_equal "Dallas", @john.partner_name + assert_equal "1997-11-1", @john.partner_birthday + end + + test "writing store attributes through accessors with prefix" do + @john.partner_name = "River" + @john.partner_birthday = "1999-2-11" + + assert_equal "River", @john.partner_name + assert_equal "1999-2-11", @john.partner_birthday + end + test "accessing attributes not exposed by accessors" do @john.settings[:icecream] = "graeters" @john.save @@ -45,7 +65,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store will mark it as changed" do @john.color = "red" - assert @john.settings_changed? + assert_predicate @john, :settings_changed? end test "updating the store populates the changed array correctly" do @@ -56,7 +76,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store won't mark it as changed if an attribute isn't changed" do @john.color = @john.color - assert !@john.settings_changed? + assert_not_predicate @john, :settings_changed? end test "object initialization with not nullable column" do @@ -137,7 +157,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store will mark it as changed encoded with JSON" do @john.height = "short" - assert @john.json_data_changed? + assert_predicate @john, :json_data_changed? end test "object initialization with not nullable column encoded with JSON" do @@ -194,4 +214,38 @@ class StoreTest < ActiveRecord::TestCase second_dump = YAML.dump(loaded) assert_equal @john, YAML.load(second_dump) end + + test "read store attributes through accessors with default suffix" do + @john.configs[:two_factor_auth] = true + assert_equal true, @john.two_factor_auth_configs + end + + test "write store attributes through accessors with default suffix" do + @john.two_factor_auth_configs = false + assert_equal false, @john.configs[:two_factor_auth] + end + + test "read store attributes through accessors with custom suffix" do + @john.configs[:login_retry] = 3 + assert_equal 3, @john.login_retry_config + end + + test "write store attributes through accessors with custom suffix" do + @john.login_retry_config = 5 + assert_equal 5, @john.configs[:login_retry] + end + + test "read accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do + @john.configs[:secret_question] = "What is your high school?" + assert_equal "What is your high school?", @john.secret_question + end + + test "write accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do + @john.secret_question = "What was the Rails version when you first worked on it?" + assert_equal "What was the Rails version when you first worked on it?", @john.configs[:secret_question] + end + + test "prefix/suffix do not affect stored attributes" do + assert_equal [:secret_question, :two_factor_auth, :login_retry], Admin::User.stored_attributes[:configs] + end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index c114842dec..a6efe3fa5e 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -6,10 +6,18 @@ require "active_record/tasks/database_tasks" module ActiveRecord module DatabaseTasksSetupper def setup - @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub - ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks - ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks - ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + @mysql_tasks, @postgresql_tasks, @sqlite_tasks = Array.new( + 3, + Class.new do + def create; end + def drop; end + def purge; end + def charset; end + def collation; end + def structure_dump(*); end + def structure_load(*); end + end.new + ) $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr @@ -18,6 +26,16 @@ module ActiveRecord def teardown $stdout, $stderr = @original_stdout, @original_stderr end + + def with_stubbed_new + ActiveRecord::Tasks::MySQLDatabaseTasks.stub(:new, @mysql_tasks) do + ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stub(:new, @postgresql_tasks) do + ActiveRecord::Tasks::SQLiteDatabaseTasks.stub(:new, @sqlite_tasks) do + yield + end + end + end + end end ADAPTERS_TASKS = { @@ -28,15 +46,16 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) protected_environments = ActiveRecord::Base.protected_environments - current_env = ActiveRecord::Migrator.current_environment + current_env = ActiveRecord::Base.connection.migration_context.current_environment assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! ActiveRecord::Base.protected_environments = [current_env] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end @@ -45,10 +64,10 @@ module ActiveRecord end def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) protected_environments = ActiveRecord::Base.protected_environments - current_env = ActiveRecord::Migrator.current_environment + current_env = ActiveRecord::Base.connection.migration_context.current_environment assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -62,11 +81,12 @@ module ActiveRecord end def test_raises_an_error_if_no_migrations_have_been_made - ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::InternalMetadata.stub(:table_exists?, false) do + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end end end @@ -79,11 +99,12 @@ module ActiveRecord end instance = klazz.new - klazz.stubs(:new).returns instance - instance.expects(:structure_dump).with("awesome-file.sql", nil) - - ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) - ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") + klazz.stub(:new, instance) do + assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do + ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") + end + end end def test_unregistered_task @@ -98,8 +119,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_create") do - eval("@#{v}").expects(:create) - ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :create) do + ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k + end + end end end end @@ -119,59 +143,89 @@ module ActiveRecord def setup @configurations = { "development" => { "database" => "my-db" } } - ActiveRecord::Base.stubs(:configurations).returns(@configurations) - # To refrain from connecting to a newly created empty DB in sqlite3_mem tests - ActiveRecord::Base.connection_handler.stubs(:establish_connection) + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_ignores_configurations_without_databases - @configurations["development"].merge!("database" => nil) - - ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + @configurations["development"]["database"] = nil - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations_establish_connection do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end end def test_ignores_remote_databases - @configurations["development"].merge!("host" => "my.server.tld") - $stderr.stubs(:puts).returns(nil) - - ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + @configurations["development"]["host"] = "my.server.tld" - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations_establish_connection do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end end def test_warning_for_remote_databases - @configurations["development"].merge!("host" => "my.server.tld") + @configurations["development"]["host"] = "my.server.tld" - $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.") + with_stubbed_configurations_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.create_all - ActiveRecord::Tasks::DatabaseTasks.create_all + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string + end end def test_creates_configurations_with_local_ip - @configurations["development"].merge!("host" => "127.0.0.1") - - ActiveRecord::Tasks::DatabaseTasks.expects(:create) + @configurations["development"]["host"] = "127.0.0.1" - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations_establish_connection do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end end def test_creates_configurations_with_local_host - @configurations["development"].merge!("host" => "localhost") - - ActiveRecord::Tasks::DatabaseTasks.expects(:create) + @configurations["development"]["host"] = "localhost" - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations_establish_connection do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end end def test_creates_configurations_with_blank_hosts - @configurations["development"].merge!("host" => nil) + @configurations["development"]["host"] = nil - ActiveRecord::Tasks::DatabaseTasks.expects(:create) - - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations_establish_connection do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end end + + private + + def with_stubbed_configurations_establish_connection + ActiveRecord::Base.stub(:configurations, @configurations) do + # To refrain from connecting to a newly created empty DB in + # sqlite3_mem tests + ActiveRecord::Base.connection_handler.stub( + :establish_connection, + nil + ) do + yield + end + end + end end class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase @@ -179,57 +233,207 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_creates_current_environment_database - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "prod-db") + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["database" => "test-db"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("production") - ) + def test_creates_current_environment_database_with_url + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["url" => "prod-db-url"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end end def test_creates_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_creates_test_and_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end - def test_establishes_connection_for_the_given_environment - ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + def test_establishes_connection_for_the_given_environments + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + private - ActiveRecord::Base.expects(:establish_connection).with(:development) + def with_stubbed_configurations_establish_connection + ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end + end - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } + end + + def test_creates_current_environment_database + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_creates_current_environment_database_with_url + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_establishes_connection_for_the_given_environments_config + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [:development] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end + + private + + def with_stubbed_configurations_establish_connection + ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class DatabaseTasksDropTest < ActiveRecord::TestCase @@ -237,8 +441,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_drop") do - eval("@#{v}").expects(:drop) - ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k + end + end end end end @@ -247,56 +454,73 @@ module ActiveRecord def setup @configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stubs(:configurations).returns(@configurations) + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_ignores_configurations_without_databases - @configurations[:development].merge!("database" => nil) - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + @configurations[:development]["database"] = nil - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end end def test_ignores_remote_databases - @configurations[:development].merge!("host" => "my.server.tld") - $stderr.stubs(:puts).returns(nil) - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end end def test_warning_for_remote_databases - @configurations[:development].merge!("host" => "my.server.tld") + @configurations[:development]["host"] = "my.server.tld" - $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.") + ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Tasks::DatabaseTasks.drop_all - ActiveRecord::Tasks::DatabaseTasks.drop_all + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string + end end def test_drops_configurations_with_local_ip - @configurations[:development].merge!("host" => "127.0.0.1") - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + @configurations[:development]["host"] = "127.0.0.1" - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end end def test_drops_configurations_with_local_host - @configurations[:development].merge!("host" => "localhost") - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + @configurations[:development]["host"] = "localhost" - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end end def test_drops_configurations_with_blank_hosts - @configurations[:development].merge!("host" => nil) + @configurations[:development]["host"] = nil - ActiveRecord::Tasks::DatabaseTasks.expects(:drop) - - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end end end @@ -305,92 +529,245 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) end def test_drops_current_environment_database - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "prod-db") + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["database" => "test-db"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("production") - ) + def test_drops_current_environment_database_with_url + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["url" => "prod-db-url"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_drops_testand_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end end - class DatabaseTasksMigrateTest < ActiveRecord::TestCase - self.use_transactional_tests = false - + class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase def setup - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = "custom/path" + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } end - def teardown - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil + def test_drops_current_environment_database + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end end - def test_migrate_receives_correct_env_vars - verbose, version = ENV["VERBOSE"], ENV["VERSION"] - - ENV["VERBOSE"] = "false" - ENV["VERSION"] = "4" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4) - ActiveRecord::Migration.expects(:verbose=).with(false) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + def test_drops_current_environment_database_with_url + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end - ENV.delete("VERBOSE") - ENV.delete("VERSION") - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end - ENV["VERBOSE"] = "" - ENV["VERSION"] = "" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" - ENV["VERBOSE"] = "yes" - ENV["VERSION"] = "0" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure - ENV["VERBOSE"], ENV["VERSION"] = verbose, version + ENV["RAILS_ENV"] = old_env + end + end + + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + # Use a memory db here to avoid having to rollback at the end + setup do + migrations_path = MIGRATIONS_ROOT + "/valid" + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", + database: ":memory:", migrations_paths: migrations_path + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end + + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end + + def test_migrate_set_and_unset_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + # run down migration because it was already run on copied db + assert_empty capture_migration_output + + ENV.delete("VERSION") + ENV.delete("VERBOSE") + + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + # run down migration because it was already run on copied db + assert_empty capture_migration_output + + ENV["VERBOSE"] = "" + ENV["VERSION"] = "" + + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + + # run down migration because it was already run on copied db + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + assert_empty capture_migration_output + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "2" + + # run no migration because 2 was already run + assert_empty capture_migration_output + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + private + def capture_migration_output + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end end + end + + class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase + self.use_transactional_tests = false def test_migrate_raise_error_on_invalid_version_format version = ENV["VERSION"] @@ -427,15 +804,16 @@ module ActiveRecord end def test_migrate_raise_error_on_failed_check_target_version - ActiveRecord::Tasks::DatabaseTasks.stubs(:check_target_version).raises("foo") - - e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } - assert_equal "foo", e.message + ActiveRecord::Tasks::DatabaseTasks.stub(:check_target_version, -> { raise "foo" }) do + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_equal "foo", e.message + end end def test_migrate_clears_schema_cache_afterward - ActiveRecord::Base.expects(:clear_cache!) - ActiveRecord::Tasks::DatabaseTasks.migrate + assert_called(ActiveRecord::Base, :clear_cache!) do + ActiveRecord::Tasks::DatabaseTasks.migrate + end end end @@ -444,8 +822,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_purge") do - eval("@#{v}").expects(:purge) - ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :purge) do + ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k + end + end end end end @@ -457,25 +838,32 @@ module ActiveRecord "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stubs(:configurations).returns(configurations) - - ActiveRecord::Tasks::DatabaseTasks.expects(:purge). - with("database" => "prod-db") - ActiveRecord::Base.expects(:establish_connection).with(:production) - - ActiveRecord::Tasks::DatabaseTasks.purge_current("production") + ActiveRecord::Base.stub(:configurations, configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") + end + end + end 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 + ActiveRecord::Base.stub(:configurations, configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end end end @@ -484,8 +872,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_charset") do - eval("@#{v}").expects(:charset) - ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :charset) do + ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k + end + end end end end @@ -495,8 +886,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_collation") do - eval("@#{v}").expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k + end + end end end end @@ -608,8 +1002,14 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_dump") do - eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil) - ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") + with_stubbed_new do + assert_called_with( + eval("@#{v}"), :structure_dump, + ["awesome-file.sql", nil] + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") + end + end end end end @@ -619,31 +1019,41 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_load") do - eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil) - ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") + with_stubbed_new do + assert_called_with( + eval("@#{v}"), + :structure_load, + ["awesome-file.sql", nil] + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") + end + end end end end class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase def test_check_schema_file - Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/)) - ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql") + assert_called_with(Kernel, :abort, [/awesome-file.sql/]) do + ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql") + end end end class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase def test_check_schema_file_defaults - ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp") - assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file + ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do + assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file + end end end class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase { ruby: "schema.rb", sql: "structure.sql" }.each_pair do |fmt, filename| define_method("test_check_schema_file_for_#{fmt}_format") do - ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp") - assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) + ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do + assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) + end end end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 047153e7cc..eeb4222d97 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -7,15 +7,11 @@ if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def create_database(*); end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -25,59 +21,97 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_without_database - ActiveRecord::Base.expects(:establish_connection). - with("adapter" => "mysql2", "database" => nil) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + [ "adapter" => "mysql2", "database" => nil ], + [ "adapter" => "mysql2", "database" => "my-app-db" ], + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_no_default_options - @connection.expects(:create_database). - with("my-app-db", {}) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :create_database, ["my-app-db", {}]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_given_encoding - @connection.expects(:create_database). - with("my-app-db", charset: "latin1") - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1") + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :create_database, ["my-app-db", charset: "latin1"]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1") + end + end end def test_creates_database_with_given_collation - @connection.expects(:create_database). - with("my-app-db", collation: "latin1_swedish_ci") - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", collation: "latin1_swedish_ci"] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci") + end + end end def test_establishes_connection_to_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + ["adapter" => "mysql2", "database" => nil], + [@configuration] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_when_database_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal "Created database 'my-app-db'\n", $stdout.string + assert_equal "Created database 'my-app-db'\n", $stdout.string + end end def test_create_when_database_exists_outputs_info_to_stderr - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::Tasks::DatabaseAlreadyExists - ) + with_stubbed_connection_establish_connection do + ActiveRecord::Base.connection.stub( + :create_database, + proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists } + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal "Database 'my-app-db' already exists\n", $stderr.string + end + end + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration + private - assert_equal "Database 'my-app-db' already exists\n", $stderr.string - end + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase def setup - @connection = stub("Connection", create_database: true) @error = Mysql2::Error.new("Invalid permissions") @configuration = { "adapter" => "mysql2", @@ -85,10 +119,6 @@ if current_adapter?(:Mysql2Adapter) "username" => "pat", "password" => "wossname" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).raises(@error) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -98,23 +128,21 @@ if current_adapter?(:Mysql2Adapter) end def test_raises_error - assert_raises(Mysql2::Error) do - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:establish_connection, -> * { raise @error }) do + assert_raises(Mysql2::Error, "Invalid permissions") do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end end end class MySQLDBDropTest < ActiveRecord::TestCase def setup - @connection = stub(drop_database: true) + @connection = Class.new { def drop_database(name); end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -124,91 +152,122 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_mysql_database - ActiveRecord::Base.expects(:establish_connection).with @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Base.expects(:establish_connection).with @configuration - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :drop_database, ["my-app-db"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end end def test_when_database_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal "Dropped database 'my-app-db'\n", $stdout.string + assert_equal "Dropped database 'my-app-db'\n", $stdout.string + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MySQLPurgeTest < ActiveRecord::TestCase def setup - @connection = stub(recreate_database: true) + @connection = Class.new { def recreate_database(*); end }.new @configuration = { "adapter" => "mysql2", "database" => "test-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_establishes_connection_to_the_appropriate_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) + with_stubbed_connection_establish_connection do + ActiveRecord::Base.expects(:establish_connection).with(@configuration) - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end def test_recreates_database_with_no_default_options - @connection.expects(:recreate_database). - with("test-db", {}) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :recreate_database, ["test-db", {}]) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end end def test_recreates_database_with_the_given_options - @connection.expects(:recreate_database). - with("test-db", charset: "latin", collation: "latin1_swedish_ci") - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( - "encoding" => "latin", "collation" => "latin1_swedish_ci") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :recreate_database, + ["test-db", charset: "latin", collation: "latin1_swedish_ci"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( + "encoding" => "latin", "collation" => "latin1_swedish_ci") + end + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MysqlDBCharsetTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def charset; end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:charset) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :charset) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end end end class MysqlDBCollationTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def collation; end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation - @connection.expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with(@connection, :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end end end @@ -222,9 +281,14 @@ if current_adapter?(:Mysql2Adapter) def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert_called_with( + Kernel, + :system, + ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end end def test_structure_dump_with_extra_flags @@ -240,41 +304,59 @@ if current_adapter?(:Mysql2Adapter) def test_structure_dump_with_ignore_tables filename = "awesome-file.sql" - ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) - - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + ActiveRecord::SchemaDumper.stub(:ignore_tables, ["foo", "bar"]) do + assert_called_with( + Kernel, + :system, + ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + end end def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" - Kernel.expects(:system) - .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db") - .returns(false) - - e = assert_raise(RuntimeError) { - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - } - assert_match(/^failed to execute: `mysqldump`$/, e.message) + assert_called_with( + Kernel, + :system, + ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"], + returns: false + ) do + e = assert_raise(RuntimeError) { + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + } + assert_match(/^failed to execute: `mysqldump`$/, e.message) + end end def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump( - @configuration.merge("port" => 10000), - filename) + assert_called_with( + Kernel, + :system, + ["mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("port" => 10000), + filename) + end end def test_structure_dump_with_ssl filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump( - @configuration.merge("sslca" => "ca.crt"), - filename) + assert_called_with( + Kernel, + :system, + ["mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("sslca" => "ca.crt"), + filename) + end end private diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index ca1defa332..00005e7a0d 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -7,15 +7,11 @@ if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def create_database(*); end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -25,82 +21,141 @@ if current_adapter?(:PostgreSQLAdapter) end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ], + [ + "adapter" => "postgresql", + "database" => "my-app-db" + ] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_default_encoding - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "utf8")] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_given_encoding - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "latin")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration. - merge("encoding" => "latin") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "latin")] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge("encoding" => "latin") + end + end end def test_creates_database_with_given_collation_and_ctype - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8", "collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration. - merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + [ + "my-app-db", + @configuration.merge( + "encoding" => "utf8", + "collation" => "ja_JP.UTF8", + "ctype" => "ja_JP.UTF8" + ) + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8") + end + end end def test_establishes_connection_to_new_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ], + [ + @configuration + ] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_db_create_with_error_prints_message - ActiveRecord::Base.stubs(:establish_connection).raises(Exception) - - $stderr.stubs(:puts).returns(true) - $stderr.expects(:puts). - with("Couldn't create database for #{@configuration.inspect}") - - assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + end + end end def test_when_database_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal "Created database 'my-app-db'\n", $stdout.string + assert_equal "Created database 'my-app-db'\n", $stdout.string + end end def test_create_when_database_exists_outputs_info_to_stderr - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::Tasks::DatabaseAlreadyExists - ) + with_stubbed_connection_establish_connection do + ActiveRecord::Base.connection.stub( + :create_database, + proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists } + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal "Database 'my-app-db' already exists\n", $stderr.string + end + end + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration + private - assert_equal "Database 'my-app-db' already exists\n", $stderr.string - end + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class PostgreSQLDBDropTest < ActiveRecord::TestCase def setup - @connection = stub(drop_database: true) + @connection = Class.new { def drop_database(*); end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -110,125 +165,192 @@ if current_adapter?(:PostgreSQLAdapter) end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection).with( + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ) + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :drop_database, + ["my-app-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end end def test_when_database_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal "Dropped database 'my-app-db'\n", $stdout.string + assert_equal "Dropped database 'my-app-db'\n", $stdout.string + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class PostgreSQLPurgeTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true, drop_database: true) + @connection = Class.new do + def create_database(*); end + def drop_database(*); end + end.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_clears_active_connections - ActiveRecord::Base.expects(:clear_active_connections!) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called(ActiveRecord::Base, :clear_active_connections!) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ], + [ + "adapter" => "postgresql", + "database" => "my-app-db" + ] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with(@connection, :drop_database, ["my-app-db"]) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_creates_database - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8")) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "utf8")] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_establishes_connection - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ], + [ + @configuration + ] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end end + + private + + def with_stubbed_connection + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end end class PostgreSQLDBCharsetTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new do + def create_database(*); end + def encoding; end + end.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:encoding) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :encoding) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end end end class PostgreSQLDBCollationTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def collation; end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation - @connection.expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end end end class PostgreSQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(schema_search_path: nil, structure_dump: true) @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } @filename = "/tmp/awesome-file.sql" FileUtils.touch(@filename) - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def teardown @@ -236,18 +358,23 @@ if current_adapter?(:PostgreSQLAdapter) end def test_structure_dump - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end end def test_structure_dump_header_comments_removed - Kernel.stubs(:system).returns(true) - File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + Kernel.stub(:system, true) do + File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) - assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + end end def test_structure_dump_with_extra_flags @@ -261,47 +388,76 @@ if current_adapter?(:PostgreSQLAdapter) end def test_structure_dump_with_ignore_tables - ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) - - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + assert_called( + ActiveRecord::SchemaDumper, + :ignore_tables, + returns: ["foo", "bar"] + ) do + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end end def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end end def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration["schema_search_path"] = "foo,bar" - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true) - - with_dump_schemas(:all) do - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"], + returns: true + ) do + with_dump_schemas(:all) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end end end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true) - - with_dump_schemas("foo,bar") do - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"], + returns: true + ) do + with_dump_schemas("foo,bar") do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end end end def test_structure_dump_execution_fails filename = "awesome-file.sql" - Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db").returns(nil) - - e = assert_raise(RuntimeError) do - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert_called_with( + Kernel, + :system, + ["pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db"], + returns: nil + ) do + e = assert_raise(RuntimeError) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + assert_match("failed to execute:", e.message) end - assert_match("failed to execute:", e.message) end private @@ -324,21 +480,22 @@ if current_adapter?(:PostgreSQLAdapter) class PostgreSQLStructureLoadTest < ActiveRecord::TestCase def setup - @connection = stub @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + assert_called_with( + Kernel, + :system, + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end end def test_structure_load_with_extra_flags @@ -354,9 +511,14 @@ if current_adapter?(:PostgreSQLAdapter) def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + assert_called_with( + Kernel, + :system, + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end end private diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index d7e3caa2ff..7eb062b456 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -9,16 +9,10 @@ if current_adapter?(:SQLite3Adapter) class SqliteDBCreateTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -28,63 +22,62 @@ if current_adapter?(:SQLite3Adapter) end def test_db_checks_database_exists - File.expects(:exist?).with(@database).returns(false) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with(File, :exist?, [@database], returns: false) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end + end end def test_when_db_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal "Created database '#{@database}'\n", $stdout.string + assert_equal "Created database '#{@database}'\n", $stdout.string + end end def test_db_create_when_file_exists - File.stubs(:exist?).returns(true) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + File.stub(:exist?, true) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal "Database '#{@database}' already exists\n", $stderr.string + assert_equal "Database '#{@database}' already exists\n", $stderr.string + end end def test_db_create_with_file_does_nothing - File.stubs(:exist?).returns(true) - $stderr.stubs(:puts).returns(nil) + File.stub(:exist?, true) do + ActiveRecord::Base.expects(:establish_connection).never - ActiveRecord::Base.expects(:establish_connection).never - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end def test_db_create_establishes_a_connection - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + assert_called_with(ActiveRecord::Base, :establish_connection, [@configuration]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end def test_db_create_with_error_prints_message - ActiveRecord::Base.stubs(:establish_connection).raises(Exception) - - $stderr.stubs(:puts).returns(true) - $stderr.expects(:puts). - with("Couldn't create database for #{@configuration.inspect}") - - assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } + ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } + assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + end end end class SqliteDBDropTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @path = stub(to_s: "/absolute/path", absolute?: true) @configuration = { "adapter" => "sqlite3", "database" => @database } - - Pathname.stubs(:new).returns(@path) - File.stubs(:join).returns("/former/relative/path") - FileUtils.stubs(:rm).returns(true) + @path = Class.new do + def to_s; "/absolute/path" end + def absolute?; true end + end.new $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr @@ -95,77 +88,76 @@ if current_adapter?(:SQLite3Adapter) end def test_creates_path_from_database - Pathname.expects(:new).with(@database).returns(@path) - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + assert_called_with(Pathname, :new, [@database], returns: @path) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end end def test_removes_file_with_absolute_path - File.stubs(:exist?).returns(true) - @path.stubs(:absolute?).returns(true) - - FileUtils.expects(:rm).with("/absolute/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + Pathname.stub(:new, @path) do + assert_called_with(FileUtils, :rm, ["/absolute/path"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end end def test_generates_absolute_path_with_given_root - @path.stubs(:absolute?).returns(false) - - File.expects(:join).with("/rails/root", @path). - returns("/former/relative/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + Pathname.stub(:new, @path) do + @path.stub(:absolute?, false) do + assert_called_with(File, :join, ["/rails/root", @path], + returns: "/former/relative/path" + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end + end end def test_removes_file_with_relative_path - File.stubs(:exist?).returns(true) - @path.stubs(:absolute?).returns(false) - - FileUtils.expects(:rm).with("/former/relative/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + File.stub(:join, "/former/relative/path") do + @path.stub(:absolute?, false) do + assert_called_with(FileUtils, :rm, ["/former/relative/path"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end + end end def test_when_db_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + FileUtils.stub(:rm, nil) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" - assert_equal "Dropped database '#{@database}'\n", $stdout.string + assert_equal "Dropped database '#{@database}'\n", $stdout.string + end end end class SqliteDBCharsetTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection + @connection = Class.new { def encoding; end }.new @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:encoding) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root" + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :encoding) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root" + end + end end end class SqliteDBCollationTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation @@ -204,9 +196,9 @@ if current_adapter?(:SQLite3Adapter) def test_structure_dump_with_ignore_tables dbfile = @database filename = "awesome-file.sql" - ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"]) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + assert_called(ActiveRecord::SchemaDumper, :ignore_tables, returns: ["foo"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + end assert File.exist?(dbfile) assert File.exist?(filename) assert_match(/bar/, File.read(filename)) @@ -219,14 +211,19 @@ if current_adapter?(:SQLite3Adapter) def test_structure_dump_execution_fails dbfile = @database filename = "awesome-file.sql" - Kernel.expects(:system).with("sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql").returns(nil) - - e = assert_raise(RuntimeError) do - with_structure_dump_flags(["--noop"]) do - quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") } + assert_called_with( + Kernel, + :system, + ["sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql"], + returns: nil + ) do + e = assert_raise(RuntimeError) do + with_structure_dump_flags(["--noop"]) do + quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") } + end end + assert_match("failed to execute:", e.message) end - assert_match("failed to execute:", e.message) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 024b5bd8a1..409b07e56c 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "active_support/test_case" +require "active_support" require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" require "active_support/testing/stream" diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 41455637bb..086500de38 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 6, Foo.columns_hash["finish"].precision end + def test_time_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 0 + @connection.add_column :foos, :finish, :time, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(start: time, finish: time) + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + end + def test_passing_precision_to_time_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 3 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 54e3f47e16..75ecd6fc40 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -90,12 +90,22 @@ class TimestampTest < ActiveRecord::TestCase @developer.touch(:created_at) end - assert !@developer.created_at_changed?, "created_at should not be changed" - assert !@developer.changed?, "record should not be changed" + assert_not @developer.created_at_changed?, "created_at should not be changed" + assert_not @developer.changed?, "record should not be changed" assert_not_equal previously_created_at, @developer.created_at assert_not_equal @previously_updated_at, @developer.updated_at end + def test_touching_update_at_attribute_as_symbol_updates_timestamp + travel(1.second) do + @developer.touch(:updated_at) + end + + assert_not @developer.updated_at_changed? + assert_not @developer.changed? + assert_not_equal @previously_updated_at, @developer.updated_at + end + def test_touching_an_attribute_updates_it task = Task.first previous_value = task.ending @@ -139,13 +149,13 @@ class TimestampTest < ActiveRecord::TestCase def test_touching_a_no_touching_object Developer.no_touching do - assert @developer.no_touching? - assert !@owner.no_touching? + assert_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? @developer.touch end - assert !@developer.no_touching? - assert !@owner.no_touching? + assert_not_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? assert_equal @previously_updated_at, @developer.updated_at end @@ -162,26 +172,26 @@ class TimestampTest < ActiveRecord::TestCase def test_global_no_touching ActiveRecord::Base.no_touching do - assert @developer.no_touching? - assert @owner.no_touching? + assert_predicate @developer, :no_touching? + assert_predicate @owner, :no_touching? @developer.touch end - assert !@developer.no_touching? - assert !@owner.no_touching? + assert_not_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? assert_equal @previously_updated_at, @developer.updated_at end def test_no_touching_threadsafe Thread.new do Developer.no_touching do - assert @developer.no_touching? + assert_predicate @developer, :no_touching? sleep(1) end end - assert !@developer.no_touching? + assert_not_predicate @developer, :no_touching? end def test_no_touching_with_callbacks @@ -237,7 +247,7 @@ class TimestampTest < ActiveRecord::TestCase pet = Pet.new(owner: klass.new) pet.save! - assert pet.owner.new_record? + assert_predicate pet.owner, :new_record? end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 1757031371..925a4609a2 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -13,7 +13,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_laster_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do - assert_not invoice.persisted? + assert_not_predicate invoice, :persisted? assert_raises(ActiveRecord::ActiveRecordError) do invoice.touch_later end @@ -23,7 +23,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_set_dirty_attributes invoice = Invoice.create! invoice.touch_later - assert_not invoice.changed? + assert_not_predicate invoice, :changed? end def test_touch_later_respects_no_touching_policy diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 1f370a80ee..c0be45eee7 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -139,6 +139,23 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [], reply.history end + def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_new_record + new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today) + add_transaction_execution_blocks new_record + + new_record.destroy + assert_equal [:commit_on_destroy], new_record.history + end + + def test_save_in_after_create_commit_wont_invoke_extra_after_create_commit + new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today) + add_transaction_execution_blocks new_record + new_record.after_commit_block(:create) { |r| r.save! } + + new_record.save! + assert_equal [:commit_on_create, :commit_on_update], new_record.history + end + def test_only_call_after_commit_on_create_and_doesnt_leaky r = ReplyWithCallbacks.new(content: "foo") r.save_on_after_create = true @@ -158,13 +175,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def test_only_call_after_commit_on_top_level_transactions @first.after_commit_block { |r| r.history << :after_commit } - assert @first.history.empty? + assert_empty @first.history @first.transaction do @first.transaction(requires_new: true) do @first.touch end - assert @first.history.empty? + assert_empty @first.history end assert_equal [:after_commit], @first.history end @@ -367,6 +384,26 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message) end + def test_after_commit_chain_not_called_on_errors + record_1 = TopicWithCallbacks.create! + record_2 = TopicWithCallbacks.create! + record_3 = TopicWithCallbacks.create! + callbacks = [] + record_1.after_commit_block { raise } + record_2.after_commit_block { callbacks << record_2.id } + record_3.after_commit_block { callbacks << record_3.id } + begin + TopicWithCallbacks.transaction do + record_1.save! + record_2.save! + record_3.save! + end + rescue + # From record_1.after_commit + end + assert_equal [], callbacks + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object pet = Pet.first owner = pet.owner @@ -394,6 +431,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end end +class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase + class PersonWithCallbacks < ActiveRecord::Base + self.table_name = :people + + after_create_commit { |record| record.history << :commit_on_create } + after_update_commit { |record| record.history << :commit_on_update } + after_destroy_commit { |record| record.history << :commit_on_destroy } + + def history + @history ||= [] + end + end + + def test_after_commit_callbacks_with_optimistic_locking + person = PersonWithCallbacks.create!(first_name: "first name") + person.update!(first_name: "another name") + person.destroy + + assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history + end +end + class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -518,7 +577,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.content = "foo" @topic.save! end - assert @topic.history.empty? + assert_empty @topic.history end def test_commit_run_transactions_callbacks_with_explicit_enrollment @@ -538,7 +597,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.save! raise ActiveRecord::Rollback end - assert @topic.history.empty? + assert_empty @topic.history end def test_rollback_run_transactions_callbacks_with_explicit_enrollment diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c110fa2f7d..46463ac414 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -20,22 +20,22 @@ class TransactionTest < ActiveRecord::TestCase def test_persisted_in_a_model_with_custom_primary_key_after_failed_save movie = Movie.create - assert !movie.persisted? + assert_not_predicate movie, :persisted? end def test_raise_after_destroy - assert_not @first.frozen? + assert_not_predicate @first, :frozen? assert_raises(RuntimeError) { Topic.transaction do @first.destroy - assert @first.frozen? + assert_predicate @first, :frozen? raise end } assert @first.reload - assert_not @first.frozen? + assert_not_predicate @first, :frozen? end def test_successful @@ -47,7 +47,7 @@ class TransactionTest < ActiveRecord::TestCase end assert Topic.find(1).approved?, "First should have been approved" - assert !Topic.find(2).approved?, "Second should have been unapproved" + assert_not Topic.find(2).approved?, "Second should have been unapproved" end def transaction_with_return @@ -80,7 +80,7 @@ class TransactionTest < ActiveRecord::TestCase assert committed assert Topic.find(1).approved?, "First should have been approved" - assert !Topic.find(2).approved?, "Second should have been unapproved" + assert_not Topic.find(2).approved?, "Second should have been unapproved" ensure Topic.connection.class_eval do remove_method :commit_db_transaction @@ -121,7 +121,7 @@ class TransactionTest < ActiveRecord::TestCase end assert Topic.find(1).approved?, "First should have been approved" - assert !Topic.find(2).approved?, "Second should have been unapproved" + assert_not Topic.find(2).approved?, "Second should have been unapproved" end def test_failing_on_exception @@ -138,9 +138,9 @@ class TransactionTest < ActiveRecord::TestCase end assert @first.approved?, "First should still be changed in the objects" - assert !@second.approved?, "Second should still be changed in the objects" + assert_not @second.approved?, "Second should still be changed in the objects" - assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert_not Topic.find(1).approved?, "First shouldn't have been approved" assert Topic.find(2).approved?, "Second should still be approved" end @@ -152,20 +152,20 @@ class TransactionTest < ActiveRecord::TestCase @first.approved = true e = assert_raises(RuntimeError) { @first.save } assert_equal "Make the transaction rollback", e.message - assert !Topic.find(1).approved? + assert_not_predicate 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 + assert_not @first.approved Topic.transaction do @first.approved = true @first.save! end - assert !Topic.find(@first.id).approved?, "Should not commit the approved flag" + assert_not Topic.find(@first.id).approved?, "Should not commit the approved flag" end def test_raising_exception_in_nested_transaction_restore_state_in_save @@ -186,7 +186,7 @@ class TransactionTest < ActiveRecord::TestCase author = Author.create! name: "foo" author.name = nil assert_not author.save - assert_not author.new_record? + assert_not_predicate author, :new_record? end def test_update_should_rollback_on_failure @@ -194,7 +194,7 @@ class TransactionTest < ActiveRecord::TestCase posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) - assert !status + assert_not status assert_equal posts_count, author.posts.reload.size end @@ -212,7 +212,7 @@ class TransactionTest < ActiveRecord::TestCase add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count status = @first.destroy - assert !status + assert_not status @first.reload assert_equal nbooks_before_destroy, Book.count end @@ -224,7 +224,7 @@ class TransactionTest < ActiveRecord::TestCase original_author_name = @first.author_name @first.author_name += "_this_should_not_end_up_in_the_db" status = @first.save - assert !status + assert_not status assert_equal original_author_name, @first.reload.author_name assert_equal nbooks_before_save, Book.count end @@ -288,7 +288,19 @@ class TransactionTest < ActiveRecord::TestCase } new_topic = topic.create(title: "A new topic") - assert !new_topic.persisted?, "The topic should not be persisted" + assert_not new_topic.persisted?, "The topic should not be persisted" + assert_nil new_topic.id, "The topic should not have an ID" + end + + def test_callback_rollback_in_create_with_rollback_exception + topic = Class.new(Topic) { + def after_create_for_transaction + raise ActiveRecord::Rollback + end + } + + new_topic = topic.create(title: "A new topic") + assert_not new_topic.persisted?, "The topic should not be persisted" assert_nil new_topic.id, "The topic should not have an ID" end @@ -303,7 +315,7 @@ class TransactionTest < ActiveRecord::TestCase end assert Topic.find(1).approved?, "First should have been approved" - assert !Topic.find(2).approved?, "Second should have been unapproved" + assert_not Topic.find(2).approved?, "Second should have been unapproved" end def test_nested_transaction_with_new_transaction_applies_parent_state_on_rollback @@ -323,8 +335,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback @@ -344,8 +356,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_double_nested_transaction_applies_parent_state_on_rollback @@ -371,9 +383,9 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? - refute_predicate topic_three, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? + assert_not_predicate topic_three, :persisted? end def test_manually_rolling_back_a_transaction @@ -387,9 +399,9 @@ class TransactionTest < ActiveRecord::TestCase end assert @first.approved?, "First should still be changed in the objects" - assert !@second.approved?, "Second should still be changed in the objects" + assert_not @second.approved?, "Second should still be changed in the objects" - assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert_not Topic.find(1).approved?, "First shouldn't have been approved" assert Topic.find(2).approved?, "Second should still be approved" end @@ -417,8 +429,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert @first.reload.approved? - assert !@second.reload.approved? + assert_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_force_savepoint_on_instance @@ -438,8 +450,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert @first.reload.approved? - assert !@second.reload.approved? + assert_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_no_savepoint_in_nested_transaction_without_force @@ -459,8 +471,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert !@first.reload.approved? - assert !@second.reload.approved? + assert_not_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_many_savepoints @@ -516,12 +528,12 @@ class TransactionTest < ActiveRecord::TestCase @first.approved = false @first.save! Topic.connection.rollback_to_savepoint("first") - assert @first.reload.approved? + assert_predicate @first.reload, :approved? @first.approved = false @first.save! Topic.connection.release_savepoint("first") - assert_not @first.reload.approved? + assert_not_predicate @first.reload, :approved? end end if Topic.connection.supports_savepoints? @@ -561,7 +573,6 @@ class TransactionTest < ActiveRecord::TestCase assert_called(Topic.connection, :begin_db_transaction) do Topic.connection.stub(:commit_db_transaction, -> { raise("OH NOES") }) do assert_called(Topic.connection, :rollback_db_transaction) do - e = assert_raise RuntimeError do Topic.transaction do # do nothing @@ -581,7 +592,7 @@ class TransactionTest < ActiveRecord::TestCase # about it since there is no specific error # for frozen objects. assert_match(/frozen/i, e.message) - assert !topic.persisted?, "not persisted" + assert_not topic.persisted?, "not persisted" assert_nil topic.id assert topic.frozen?, "not frozen" end @@ -608,9 +619,9 @@ class TransactionTest < ActiveRecord::TestCase thread.join assert @first.approved?, "First should still be changed in the objects" - assert !@second.approved?, "Second should still be changed in the objects" + assert_not @second.approved?, "Second should still be changed in the objects" - assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert_not Topic.find(1).approved?, "First shouldn't have been approved" assert Topic.find(2).approved?, "Second should still be approved" end @@ -641,15 +652,15 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - assert !topic_1.persisted?, "not persisted" + assert_not topic_1.persisted?, "not persisted" assert_nil topic_1.id - assert !topic_2.persisted?, "not persisted" + assert_not topic_2.persisted?, "not persisted" assert_nil topic_2.id - assert !topic_3.persisted?, "not persisted" + assert_not topic_3.persisted?, "not persisted" assert_nil topic_3.id assert @first.persisted?, "persisted" assert_not_nil @first.id - assert !@second.destroyed?, "not destroyed" + assert_not @second.destroyed?, "not destroyed" end def test_restore_frozen_state_after_double_destroy @@ -663,8 +674,38 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - assert_not reply.frozen? - assert_not topic.frozen? + assert_not_predicate reply, :frozen? + assert_not_predicate topic, :frozen? + end + + def test_restore_new_record_after_double_save + topic = Topic.new + + Topic.transaction do + topic.save! + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.id + assert_predicate topic, :new_record? + end + + def test_dont_restore_new_record_in_subsequent_transaction + topic = Topic.new + + Topic.transaction do + topic.save! + topic.save! + end + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_predicate topic, :persisted? + assert_not_predicate topic, :new_record? end def test_restore_id_after_rollback @@ -819,28 +860,28 @@ class TransactionTest < ActiveRecord::TestCase connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - assert transaction.open? - assert !transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction, :open? + assert_not_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? transaction.rollback - assert transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? end def test_transactions_state_from_commit connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - assert transaction.open? - assert !transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction, :open? + assert_not_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? transaction.commit - assert !transaction.state.rolledback? - assert transaction.state.committed? + assert_not_predicate transaction.state, :rolledback? + assert_predicate transaction.state, :committed? end def test_set_state_method_is_deprecated @@ -929,7 +970,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase raise end rescue - assert !@first.reload.approved? + assert_not_predicate @first.reload, :approved? end end @@ -950,7 +991,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase end end - assert !@first.reload.approved? + assert_not_predicate @first.reload, :approved? end end if Topic.connection.supports_savepoints? diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 8c51b30fdd..9e7810a6a5 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -9,16 +9,16 @@ module ActiveRecord klass.table_name = "authors" author = klass.create!(name: "Sean") - assert_not author.changed? + assert_not_predicate author, :changed? author.name << " Griffin" - assert author.name_changed? + assert_predicate author, :name_changed? author.save! author.reload assert_equal "Sean Griffin", author.name - assert_not author.changed? + assert_not_predicate author, :changed? end end end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index f4d8be5897..9eefc32745 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -30,6 +30,6 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase end def test_underlying_adapter_no_longer_active - assert !@underlying.active?, "Removed adapter should no longer be active" + assert_not @underlying.active?, "Removed adapter should no longer be active" end end diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb index 72d4997d0b..d5d8f2a09a 100644 --- a/activerecord/test/cases/unsafe_raw_sql_test.rb +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -107,6 +107,26 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal ids_expected, ids_disabled end + test "order: allows NULLS FIRST and NULLS LAST too" do + raise "precondition failed" if Post.count < 2 + + # Ensure there are NULL and non-NULL post types. + Post.first.update_column(:type, nil) + Post.last.update_column(:type, "Programming") + + ["asc", "desc", ""].each do |direction| + %w(first last).each do |position| + ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + end + end if current_adapter?(:PostgreSQLAdapter) + test "order: disallows invalid column name" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index a997f8be9c..8235a54d8a 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -13,8 +13,8 @@ class AbsenceValidationTest < ActiveRecord::TestCase validates_absence_of :name end - assert boy_klass.new.valid? - assert_not boy_klass.new(name: "Alex").valid? + assert_predicate boy_klass.new, :valid? + assert_not_predicate boy_klass.new(name: "Alex"), :valid? end def test_has_one_marked_for_destruction @@ -44,7 +44,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase assert_not boy.valid?, "should not be valid if has_many association is present" i2.mark_for_destruction - assert boy.valid? + assert_predicate boy, :valid? end def test_does_not_call_to_a_on_associations @@ -65,11 +65,11 @@ class AbsenceValidationTest < ActiveRecord::TestCase Interest.validates_absence_of(:token) interest = Interest.create!(topic: "Thought Leadering") - assert interest.valid? + assert_predicate interest, :valid? interest.token = "tl" - assert interest.invalid? + assert_predicate interest, :invalid? end end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 80fe375ae5..ce6d42b34b 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -16,14 +16,14 @@ class AssociationValidationTest < ActiveRecord::TestCase Reply.validates_presence_of(:content) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] - assert !t.valid? - assert t.errors[:replies].any? + assert_not_predicate t, :valid? + assert_predicate t.errors[:replies], :any? assert_equal 1, r.errors.count # make sure all associated objects have been validated assert_equal 0, r2.errors.count assert_equal 1, r3.errors.count assert_equal 0, r4.errors.count r.content = r3.content = "non-empty" - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_one @@ -31,10 +31,10 @@ class AssociationValidationTest < ActiveRecord::TestCase Topic.validates_presence_of(:content) r = Reply.new("title" => "A reply", "content" => "with content!") r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert r.errors[:topic].any? + assert_not_predicate r, :valid? + assert_predicate r.errors[:topic], :any? r.topic.content = "non-empty" - assert r.valid? + assert_predicate r, :valid? end def test_validates_associated_marked_for_destruction @@ -42,9 +42,9 @@ class AssociationValidationTest < ActiveRecord::TestCase Reply.validates_presence_of(:content) t = Topic.new t.replies << Reply.new - assert t.invalid? + assert_predicate t, :invalid? t.replies.first.mark_for_destruction - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_without_marked_for_destruction @@ -56,7 +56,7 @@ class AssociationValidationTest < ActiveRecord::TestCase Topic.validates_associated(:replies) t = Topic.new t.define_singleton_method(:replies) { [reply.new] } - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_with_custom_message_using_quotes @@ -71,11 +71,11 @@ class AssociationValidationTest < ActiveRecord::TestCase def test_validates_associated_missing Reply.validates_presence_of(:topic) r = Reply.create("title" => "A reply", "content" => "with content!") - assert !r.valid? - assert r.errors[:topic].any? + assert_not_predicate r, :valid? + assert_predicate r.errors[:topic], :any? r.topic = Topic.first - assert r.valid? + assert_predicate r, :valid? end def test_validates_presence_of_belongs_to_association__parent_is_new_record diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 87ce4c6f37..1fbcdc271b 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -17,48 +17,48 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 } o = @owner.new("name" => "nopets") - assert !o.save - assert o.errors[:pets].any? + assert_not o.save + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") - assert o.valid? + assert_predicate o, :valid? end def test_validates_size_of_association_using_within assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 } o = @owner.new("name" => "nopets") - assert !o.save - assert o.errors[:pets].any? + assert_not o.save + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") - assert o.valid? + assert_predicate o, :valid? 2.times { o.pets.build("name" => "apet") } - assert !o.save - assert o.errors[:pets].any? + assert_not o.save + assert_predicate o.errors[:pets], :any? end def test_validates_size_of_association_utf8 @owner.validates_size_of :pets, minimum: 1 o = @owner.new("name" => "あいうえおかきくけこ") - assert !o.save - assert o.errors[:pets].any? + assert_not o.save + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "あいうえおかきくけこ") - assert o.valid? + assert_predicate o, :valid? end def test_validates_size_of_respects_records_marked_for_destruction @owner.validates_size_of :pets, minimum: 1 owner = @owner.new assert_not owner.save - assert owner.errors[:pets].any? + assert_predicate owner.errors[:pets], :any? pet = owner.pets.build - assert owner.valid? + assert_predicate owner, :valid? assert owner.save pet_count = Pet.count - assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ] - assert_not owner.valid? - assert owner.errors[:pets].any? + assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ] + assert_not_predicate owner, :valid? + assert_predicate owner.errors[:pets], :any? assert_equal pet_count, Pet.count end @@ -70,11 +70,11 @@ class LengthValidationTest < ActiveRecord::TestCase pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy") - assert pet.valid? + assert_predicate pet, :valid? pet.nickname = "" - assert pet.invalid? + assert_predicate pet, :invalid? end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3ab1567b51..63c3f67da2 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -15,10 +15,10 @@ class PresenceValidationTest < ActiveRecord::TestCase def test_validates_presence_of_non_association Boy.validates_presence_of(:name) b = Boy.new - assert b.invalid? + assert_predicate b, :invalid? b.name = "Alex" - assert b.valid? + assert_predicate b, :valid? end def test_validates_presence_of_has_one @@ -33,23 +33,23 @@ class PresenceValidationTest < ActiveRecord::TestCase b = Boy.new f = Face.new b.face = f - assert b.valid? + assert_predicate b, :valid? f.mark_for_destruction - assert b.invalid? + assert_predicate b, :invalid? end def test_validates_presence_of_has_many_marked_for_destruction Boy.validates_presence_of(:interests) b = Boy.new b.interests << [i1 = Interest.new, i2 = Interest.new] - assert b.valid? + assert_predicate b, :valid? i1.mark_for_destruction - assert b.valid? + assert_predicate b, :valid? i2.mark_for_destruction - assert b.invalid? + assert_predicate b, :invalid? end def test_validates_presence_doesnt_convert_to_array @@ -74,11 +74,11 @@ class PresenceValidationTest < ActiveRecord::TestCase Interest.validates_presence_of(:abbreviation) interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl") - assert interest.valid? + assert_predicate interest, :valid? interest.abbreviation = "" - assert interest.invalid? + assert_predicate interest, :invalid? end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index a10567f066..8f6f47e5fb 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -62,7 +62,7 @@ class TopicWithAfterCreate < Topic after_create :set_author def set_author - update_attributes!(author_name: "#{title} #{id}") + update!(author_name: "#{title} #{id}") end end @@ -83,8 +83,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t.save, "Should still save t as unique" t2 = Topic.new("title" => "I'm uniqué!") - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" + assert_not t2.valid?, "Shouldn't be valid" + assert_not t2.save, "Shouldn't save t2 as unique" assert_equal ["has already been taken"], t2.errors[:title] t2.title = "Now I am really also unique" @@ -96,7 +96,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.validates_uniqueness_of(:new_title) topic = Topic.new(new_title: "abc") - assert topic.valid? + assert_predicate topic, :valid? end def test_validates_uniqueness_with_nil_value @@ -106,8 +106,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t.save, "Should save t as unique" t2 = Topic.new("title" => nil) - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" + assert_not t2.valid?, "Shouldn't be valid" + assert_not t2.save, "Shouldn't save t2 as unique" assert_equal ["has already been taken"], t2.errors[:title] end @@ -116,7 +116,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.create!("title" => "abc") t2 = Topic.new("title" => "abc") - assert !t2.valid? + assert_not_predicate t2, :valid? assert t2.errors[:title] end @@ -146,7 +146,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = t.replies.create "title" => "r2", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" + assert_not r2.valid?, "Saving r2 first time" r2.content = "something else" assert r2.save, "Saving r2 second time" @@ -172,7 +172,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = t.replies.create "title" => "r2", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" + assert_not r2.valid?, "Saving r2 first time" end def test_validate_uniqueness_with_polymorphic_object_scope @@ -193,7 +193,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" + assert_not r2.valid?, "Saving r2 first time" end def test_validate_uniqueness_with_object_arg @@ -205,7 +205,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = t.replies.create "title" => "r2", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" + assert_not r2.valid?, "Saving r2 first time" end def test_validate_uniqueness_scoped_to_defining_class @@ -215,7 +215,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun" - assert !r2.valid?, "Saving r2" + assert_not r2.valid?, "Saving r2" # Should succeed as validates_uniqueness_of only applies to # UniqueReply and its subclasses @@ -232,19 +232,19 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r1.valid?, "Saving r1" r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." - assert !r2.valid?, "Saving r2. Double reply by same author." + assert_not r2.valid?, "Saving r2. Double reply by same author." r2.author_email_address = "jeremy_alt_email@rubyonrails.com" assert r2.save, "Saving r2 the second time." r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" - assert !r3.valid?, "Saving r3" + assert_not r3.valid?, "Saving r3" r3.author_name = "jj" assert r3.save, "Saving r3 the second time." r3.author_name = "jeremy" - assert !r3.save, "Saving r3 the third time." + assert_not r3.save, "Saving r3 the third time." end def test_validate_case_insensitive_uniqueness @@ -257,17 +257,17 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t.save, "Should still save t as unique" t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors[:title].any? - assert t2.errors[:parent_id].any? + assert_not t2.valid?, "Shouldn't be valid" + assert_not t2.save, "Shouldn't save t2 as unique" + assert_predicate t2.errors[:title], :any? + assert_predicate t2.errors[:parent_id], :any? assert_equal ["has already been taken"], t2.errors[:title] t2.title = "I'm truly UNIQUE!" - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors[:title].empty? - assert t2.errors[:parent_id].any? + assert_not t2.valid?, "Shouldn't be valid" + assert_not t2.save, "Shouldn't save t2 as unique" + assert_empty t2.errors[:title] + assert_predicate t2.errors[:parent_id], :any? t2.parent_id = 4 assert t2.save, "Should now save t2 as unique" @@ -283,8 +283,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase # If database hasn't UTF-8 character set, this test fails 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" + assert_not t2_utf8.valid?, "Shouldn't be valid" + assert_not t2_utf8.save, "Shouldn't save t2_utf8 as unique" end end @@ -326,15 +326,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase t2 = Topic.new("title" => "I'M UNIQUE!") assert t2.valid?, "Should be valid" assert t2.save, "Should save t2 as unique" - assert t2.errors[:title].empty? - assert t2.errors[:parent_id].empty? + assert_empty t2.errors[:title] + assert_empty t2.errors[:parent_id] assert_not_equal ["has already been taken"], t2.errors[:title] t3 = Topic.new("title" => "I'M uNiQUe!") assert t3.valid?, "Should be valid" assert t3.save, "Should save t2 as unique" - assert t3.errors[:title].empty? - assert t3.errors[:parent_id].empty? + assert_empty t3.errors[:title] + assert_empty t3.errors[:parent_id] assert_not_equal ["has already been taken"], t3.errors[:title] end @@ -343,13 +343,13 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.create!("title" => 101) t2 = Topic.new("title" => 101) - assert !t2.valid? + assert_not_predicate t2, :valid? assert t2.errors[:title] end def test_validate_uniqueness_with_non_standard_table_names i1 = WarehouseThing.create(value: 1000) - assert !i1.valid?, "i1 should not be valid" + assert_not i1.valid?, "i1 should not be valid" assert i1.errors[:value].any?, "Should not be empty" end @@ -360,7 +360,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") assert t1.save t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") - assert !t2.valid? + assert_not_predicate t2, :valid? end end @@ -417,12 +417,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase # Should use validation from base class (which is abstract) w2 = IneptWizard.new(name: "Rincewind", city: "Quirm") - assert !w2.valid?, "w2 shouldn't be valid" + assert_not w2.valid?, "w2 shouldn't be valid" assert w2.errors[:name].any?, "Should have errors for name" assert_equal ["has already been taken"], w2.errors[:name], "Should have uniqueness message for name" w3 = Conjurer.new(name: "Rincewind", city: "Quirm") - assert !w3.valid?, "w3 shouldn't be valid" + assert_not w3.valid?, "w3 shouldn't be valid" assert w3.errors[:name].any?, "Should have errors for name" assert_equal ["has already been taken"], w3.errors[:name], "Should have uniqueness message for name" @@ -430,12 +430,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert w4.valid?, "Saving w4" w5 = Thaumaturgist.new(name: "The Amazing Bonko", city: "Lancre") - assert !w5.valid?, "w5 shouldn't be valid" + assert_not w5.valid?, "w5 shouldn't be valid" assert w5.errors[:name].any?, "Should have errors for name" assert_equal ["has already been taken"], w5.errors[:name], "Should have uniqueness message for name" w6 = Thaumaturgist.new(name: "Mustrum Ridcully", city: "Quirm") - assert !w6.valid?, "w6 shouldn't be valid" + assert_not w6.valid?, "w6 shouldn't be valid" assert w6.errors[:city].any?, "Should have errors for city" assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" end @@ -446,7 +446,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.create("title" => "I'm an unapproved topic", "approved" => false) t3 = Topic.new("title" => "I'm a topic", "approved" => true) - assert !t3.valid?, "t3 shouldn't be valid" + assert_not t3.valid?, "t3 shouldn't be valid" t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false) assert t4.valid?, "t4 should be valid" @@ -460,16 +460,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validate_uniqueness_on_existing_relation event = Event.create - assert TopicWithUniqEvent.create(event: event).valid? + assert_predicate TopicWithUniqEvent.create(event: event), :valid? topic = TopicWithUniqEvent.new(event: event) - assert_not topic.valid? + assert_not_predicate topic, :valid? assert_equal ["has already been taken"], topic.errors[:event] end def test_validate_uniqueness_on_empty_relation topic = TopicWithUniqEvent.new - assert topic.valid? + assert_predicate topic, :valid? end def test_validate_uniqueness_of_custom_primary_key @@ -488,7 +488,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase key2 = klass.create!(key_number: 11) key2.key_number = 10 - assert_not key2.valid? + assert_not_predicate key2, :valid? end def test_validate_uniqueness_without_primary_key @@ -501,8 +501,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase end abc = klass.create!(dashboard_id: "abc") - assert klass.new(dashboard_id: "xyz").valid? - assert_not klass.new(dashboard_id: "abc").valid? + assert_predicate klass.new(dashboard_id: "xyz"), :valid? + assert_not_predicate klass.new(dashboard_id: "abc"), :valid? abc.dashboard_id = "def" @@ -530,7 +530,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert topic.author_name.start_with?("Title1") topic2 = TopicWithAfterCreate.new(title: "Title1") - refute topic2.valid? + assert_not_predicate topic2, :valid? assert_equal(["has already been taken"], topic2.errors[:title]) end @@ -550,7 +550,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert_empty item.errors item2 = CoolTopic.new(id: item.id, title: "MyItem2") - refute item2.valid? + assert_not_predicate item2, :valid? assert_equal(["has already been taken"], item2.errors[:id]) end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 14623c43d2..a33877f43a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -19,7 +19,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_valid_uses_create_context_when_new r = WrongReply.new r.title = "Wrong Create" - assert_not r.valid? + assert_not_predicate 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 @@ -39,7 +39,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_valid_using_special_context r = WrongReply.new(title: "Valid title") - assert !r.valid?(:special_case) + assert_not r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join r.author_name = "secret" @@ -125,7 +125,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_save_without_validation reply = WrongReply.new - assert !reply.save + assert_not reply.save assert reply.save(validate: false) end @@ -139,7 +139,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_throw_away_typing d = Developer.new("name" => "David", "salary" => "100,000") - assert !d.valid? + assert_not_predicate d, :valid? assert_equal 100, d.salary assert_equal "100,000", d.salary_before_type_cast end @@ -166,7 +166,7 @@ class ValidationsTest < ActiveRecord::TestCase topic = klass.new(wibble: "123-4567") topic.wibble.gsub!("-", "") - assert topic.valid? + assert_predicate topic, :valid? end def test_numericality_validation_checks_against_raw_value @@ -178,9 +178,9 @@ class ValidationsTest < ActiveRecord::TestCase validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal("97.18") end - assert_not klass.new(wibble: "97.179").valid? - assert_not klass.new(wibble: 97.179).valid? - assert_not klass.new(wibble: BigDecimal("97.179")).valid? + assert_not_predicate klass.new(wibble: "97.179"), :valid? + assert_not_predicate klass.new(wibble: 97.179), :valid? + assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid? end def test_acceptance_validator_doesnt_require_db_connection diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 578881f754..60ebdce178 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -96,7 +96,7 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_deserializing_rails_41_yaml topic = YAML.load(yaml_fixture("rails_4_1")) - assert topic.new_record? + assert_predicate topic, :new_record? assert_nil topic.id assert_equal "The First Topic", topic.title assert_equal({ omg: :lol }, topic.content) @@ -105,7 +105,7 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_deserializing_rails_4_2_0_yaml topic = YAML.load(yaml_fixture("rails_4_2_0")) - assert_not topic.new_record? + assert_not_predicate topic, :new_record? assert_equal 1, topic.id assert_equal "The First Topic", topic.title assert_equal("Have a nice day", topic.content) diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore deleted file mode 100644 index 885029a512..0000000000 --- a/activerecord/test/fixtures/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.sqlite*
\ No newline at end of file diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml index 0399ff83b9..7d6c1366d0 100644 --- a/activerecord/test/fixtures/customers.yml +++ b/activerecord/test/fixtures/customers.yml @@ -23,4 +23,13 @@ barney: address_street: Quiet Road address_city: Peaceful Town address_country: Tranquil Land - gps_location: NULL
\ No newline at end of file + gps_location: NULL + +mary: + id: 4 + name: Mary + balance: 1 + address_street: Funny Street + address_city: Peaceful Town + address_country: Nation Land + gps_location: NULL diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml index a5d52bd438..f7ca227533 100644 --- a/activerecord/test/fixtures/memberships.yml +++ b/activerecord/test/fixtures/memberships.yml @@ -26,6 +26,13 @@ blarpy_winkup_crazy_club: favourite: false type: CurrentMembership +super_membership_of_boring_club: + joined_on: <%= 3.weeks.ago.to_s(:db) %> + club: boring_club + member_id: 1 + favourite: false + type: SuperMembership + selected_membership_of_boring_club: joined_on: <%= 3.weeks.ago.to_s(:db) %> club: boring_club diff --git a/activerecord/test/fixtures/minimalistics.yml b/activerecord/test/fixtures/minimalistics.yml index c3ec546209..83df0551bc 100644 --- a/activerecord/test/fixtures/minimalistics.yml +++ b/activerecord/test/fixtures/minimalistics.yml @@ -1,2 +1,5 @@ +zero: + id: 0 + first: id: 1 diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index 2da541c539..02ddb8dd38 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho: sponsor_club: crazy_club sponsorable_id: 3 sponsorable_type: Member +sponsor_for_author_david: + sponsorable_id: 1 + sponsorable_type: Author diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml deleted file mode 100644 index ff515beb45..0000000000 --- a/activerecord/test/fixtures/teapots.yml +++ /dev/null @@ -1,3 +0,0 @@ -bob: - id: 1 - name: Bob diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb index b892f50e41..7d4233fe31 100644 --- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb +++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb @@ -5,7 +5,7 @@ class GiveMeBigNumbers < ActiveRecord::Migration::Current create_table :big_numbers do |table| table.column :bank_balance, :decimal, precision: 10, scale: 2 table.column :big_bank_balance, :decimal, precision: 15, scale: 2 - table.column :world_population, :decimal, precision: 10 + table.column :world_population, :decimal, precision: 20 table.column :my_house_population, :decimal, precision: 2 table.column :value_of_e, :decimal end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index abb5cb28e7..691f9f11be 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -19,6 +19,12 @@ class Admin::User < ActiveRecord::Base store :params, accessors: [ :token ], coder: YAML store :settings, accessors: [ :color, :homepage ] store_accessor :settings, :favorite_food + store :parent, accessors: [:birthday, :name], prefix: true + store :spouse, accessors: [:birthday], prefix: :partner + store_accessor :spouse, :name, prefix: :partner + store :configs, accessors: [ :secret_question ] + store :configs, accessors: [ :two_factor_auth ], suffix: true + store_accessor :configs, :login_retry, suffix: :config store :preferences, accessors: [ :remember_login ] store :json_data, accessors: [ :height, :weight ], coder: Coder.new store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index cb8686f315..75932c7eb6 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -8,6 +8,7 @@ class Author < ActiveRecord::Base has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post" has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post" has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post" + has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post" has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post" has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post" has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post" @@ -40,7 +41,7 @@ class Author < ActiveRecord::Base -> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) }, class_name: "Post" - has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, source: :comments + has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments has_many :funky_comments, through: :posts, source: :comments has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments @@ -88,6 +89,9 @@ class Author < ActiveRecord::Base has_many :special_categories, through: :special_categorizations, source: :category has_one :special_category, through: :special_categorizations, source: :category + has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category + has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category + has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category" has_many :categorized_posts, through: :categorizations, source: :post @@ -158,6 +162,9 @@ class Author < ActiveRecord::Base def extension_method; end end + has_many :top_posts, -> { order(id: :asc) }, class_name: "Post" + has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post" + attr_accessor :post_log after_initialize :set_post_log diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 3d6a7a96c2..8614926626 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -20,6 +20,8 @@ class Car < ActiveRecord::Base scope :incl_engines, -> { includes(:engines) } scope :order_using_new_style, -> { order("name asc") } + + attribute :wheels_owned_at, :datetime, default: -> { Time.now } end class CoolCar < Car diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 5ab433f2d9..f0f0576709 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -76,7 +76,7 @@ class CommentThatAutomaticallyAltersPostBody < Comment belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id after_save do |comment| - comment.post.update_attributes(body: "Automatically altered") + comment.post.update(body: "Automatically altered") end end @@ -87,6 +87,6 @@ end class CommentWithAfterCreateUpdate < Comment after_create do - update_attributes(body: "bar") + update(body: "bar") end end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index fc6488f729..d4d5275b78 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -145,6 +145,21 @@ class Client < Company raise RaisedOnSave if raise_on_save end + attr_accessor :throw_on_save + before_save do + throw :abort if throw_on_save + end + + attr_accessor :rollback_on_save + after_save do + raise ActiveRecord::Rollback if rollback_on_save + end + + attr_accessor :rollback_on_create_called + after_rollback(on: :create) do |client| + client.rollback_on_create_called = true + end + class RaisedOnDestroy < RuntimeError; end attr_accessor :raise_on_destroy before_destroy do diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 524a9d7bd9..bc501a5ce0 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -4,7 +4,7 @@ class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run composed_of :address, mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ], allow_nil: true - composed_of :balance, class_name: "Money", mapping: %w(balance amount), converter: Proc.new(&:to_money) + composed_of :balance, class_name: "Money", mapping: %w(balance amount) composed_of :gps_location, allow_nil: true composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location), converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) } diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 948435136d..e900fd40fb 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -2,6 +2,7 @@ class Face < ActiveRecord::Base belongs_to :man, inverse_of: :face + belongs_to :human, polymorphic: true belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` belongs_to :poly_man_without_inverse, polymorphic: true diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb new file mode 100644 index 0000000000..73601aacdd --- /dev/null +++ b/activerecord/test/models/frog.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Frog < ActiveRecord::Base + after_save do + with_lock do + end + end +end diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index 3acd89a48e..e26920e951 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -11,3 +11,6 @@ class Man < ActiveRecord::Base has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man has_one :mixed_case_monkey end + +class Human < Man +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 4315ba1941..6e33ac0a6d 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -26,13 +26,14 @@ class Member < ActiveRecord::Base has_one :club_category, through: :club, source: :category has_one :general_club, -> { general }, through: :current_membership, source: :club - has_many :current_memberships, -> { where favourite: true } - has_many :clubs, through: :current_memberships + has_many :super_memberships + has_many :favourite_memberships, -> { where(favourite: true) }, class_name: "Membership" + has_many :clubs, through: :favourite_memberships has_many :tenant_memberships has_many :tenant_clubs, through: :tenant_memberships, class_name: "Club", source: :club - has_one :club_through_many, through: :current_memberships, source: :club + has_one :club_through_many, through: :favourite_memberships, source: :club belongs_to :admittable, polymorphic: true has_one :premium_club, through: :admittable diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 87f7aab9a2..e121a849d0 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -5,6 +5,7 @@ class MemberDetail < ActiveRecord::Base belongs_to :organization has_one :member_type, through: :member has_one :membership, through: :member + has_one :admittable, through: :member, source_type: "Member" has_many :organization_member_details, through: :organization, source: :member_details end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index c8617d1cfe..fd5083e597 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -17,7 +17,13 @@ class Pirate < ActiveRecord::Base after_remove: proc { |p, pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}" } has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true - has_many :treasures, as: :looter + module PostTreasuresExtension + def build(attributes = {}) + super({ name: "from extension" }.merge(attributes)) + end + end + + has_many :treasures, as: :looter, extend: PostTreasuresExtension has_many :treasure_estimates, through: :treasures, source: :price_estimates has_one :ship diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 780a2c17f5..640cdb33b4 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -106,6 +106,9 @@ class Post < ActiveRecord::Base end end + has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count + has_many :indestructible_tags, through: :indestructible_taggings, source: :tag + has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count @@ -250,6 +253,7 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" default_scope { where(id: [1, 5, 6]) } + scope :unscoped_all, -> { unscoped { all } } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base @@ -323,5 +327,13 @@ class FakeKlass def enforce_raw_sql_whitelist(*args) # noop end + + def arel_table + Post.arel_table + end + + def predicate_builder + Post.predicate_builder + end end end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index bc829ec67f..0ea110f4f8 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -4,8 +4,9 @@ require "models/topic" class Reply < Topic belongs_to :topic, foreign_key: "parent_id", counter_cache: true - belongs_to :topic_with_primary_key, class_name: "Topic", primary_key: "title", foreign_key: "parent_title", counter_cache: "replies_count" + belongs_to :topic_with_primary_key, class_name: "Topic", primary_key: "title", foreign_key: "parent_title", counter_cache: "replies_count", touch: true has_many :replies, class_name: "SillyReply", dependent: :destroy, foreign_key: "parent_id" + has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id" end class UniqueReply < Reply @@ -14,6 +15,7 @@ class UniqueReply < Reply end class SillyUniqueReply < UniqueReply + validates :content, uniqueness: true end class WrongReply < Reply diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index f190860fd1..18ff103ffe 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -3,6 +3,7 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id" belongs_to :sponsorable, polymorphic: true + belongs_to :sponsor, polymorphic: true belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true, foreign_type: "sponsorable_type", foreign_key: "sponsorable_id" diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 861fde633f..6d4230f6f4 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -14,3 +14,7 @@ class Tagging < ActiveRecord::Base belongs_to :taggable, polymorphic: true, counter_cache: :tags_count has_many :things, through: :taggable end + +class IndestructibleTagging < Tagging + before_destroy { throw :abort } +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 8cd4dc352a..72699046f9 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -12,9 +12,17 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { all } + scope :by_private_lifo, -> { where(author_name: private_lifo) } scope :by_lifo, -> { where(author_name: "lifo") } scope :replied, -> { where "replies_count > 0" } + class << self + private + def private_lifo + "lifo" + end + end + scope "approved_as_string", -> { where(approved: true) } scope :anonymous_extension, -> {} do def one @@ -73,6 +81,16 @@ class Topic < ActiveRecord::Base self.class.after_initialize_called = true end + attr_accessor :after_touch_called + + after_initialize do + self.after_touch_called = 0 + end + + after_touch do + self.after_touch_called += 1 + end + def approved=(val) @custom_approved = val write_attribute(:approved, val) @@ -89,7 +107,7 @@ class Topic < ActiveRecord::Base end def set_email_address - unless persisted? + unless persisted? || will_save_change_to_author_email_address? self.author_email_address = "test@test.com" end end diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb index 8db57d181e..22fc74995f 100644 --- a/activerecord/test/models/wheel.rb +++ b/activerecord/test/models/wheel.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Wheel < ActiveRecord::Base - belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: true + belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: :wheels_owned_at end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index e634e9e6b1..8371ba9528 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - - if ActiveRecord::Base.connection.version >= "5.6.0" + if subsecond_precision_supported? create_table :datetime_defaults, force: true do |t| t.datetime :modified_datetime, default: -> { "CURRENT_TIMESTAMP" } + t.datetime :precise_datetime, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" } end - end - create_table :timestamp_defaults, force: true do |t| - t.timestamp :nullable_timestamp - t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + create_table :timestamp_defaults, force: true do |t| + t.timestamp :nullable_timestamp + t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + t.timestamp :precise_timestamp, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" } + end end create_table :binary_fields, force: true do |t| diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index e236571caa..bc1e45ca80 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - execute "drop table test_oracle_defaults" rescue nil execute "drop sequence test_oracle_defaults_seq" rescue nil execute "drop sequence companies_nonstd_seq" rescue nil @@ -38,5 +37,4 @@ create sequence test_oracle_defaults_seq minvalue 10000 ) SQL execute "create sequence defaults_seq minvalue 10000" - end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f15178d695..975824ed51 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - enable_extension!("uuid-ossp", ActiveRecord::Base.connection) enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid? diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 3205c4c20a..266e55f682 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -21,6 +21,9 @@ ActiveRecord::Schema.define do create_table :admin_users, force: true do |t| t.string :name t.string :settings, null: true, limit: 1024 + t.string :parent, null: true, limit: 1024 + t.string :spouse, null: true, limit: 1024 + t.string :configs, null: true, limit: 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. t.string :preferences, null: true, default: "", limit: 1024 @@ -33,6 +36,7 @@ ActiveRecord::Schema.define do create_table :aircraft, force: true do |t| t.string :name t.integer :wheels_count, default: 0, null: false + t.datetime :wheels_owned_at end create_table :articles, force: true do |t| @@ -123,7 +127,8 @@ ActiveRecord::Schema.define do create_table :cars, force: true do |t| t.string :name t.integer :engines_count - t.integer :wheels_count, default: 0 + t.integer :wheels_count, default: 0, null: false + t.datetime :wheels_owned_at t.column :lock_version, :integer, null: false, default: 0 t.timestamps null: false end @@ -210,7 +215,7 @@ ActiveRecord::Schema.define do t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree - t.index "lower(name)", name: "company_expression_index" if supports_expression_index? + t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index? end create_table :content, force: true do |t| @@ -345,6 +350,10 @@ ActiveRecord::Schema.define do t.string :token end + create_table :frogs, force: true do |t| + t.string :name + end + create_table :funny_jokes, force: true do |t| t.string :name end @@ -480,7 +489,8 @@ ActiveRecord::Schema.define do create_table :members, force: true do |t| t.string :name - t.integer :member_type_id + t.references :member_type, index: false + t.references :admittable, polymorphic: true, index: false end create_table :member_details, force: true do |t| @@ -547,7 +557,7 @@ ActiveRecord::Schema.define do create_table :numeric_data, force: true do |t| t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :big_bank_balance, precision: 15, scale: 2 - t.decimal :world_population, precision: 10, scale: 0 + t.decimal :world_population, precision: 20, scale: 0 t.decimal :my_house_population, precision: 2, scale: 0 t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78 t.float :temperature @@ -690,6 +700,7 @@ ActiveRecord::Schema.define do t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 + t.integer :indestructible_tags_count, default: 0 t.integer :tags_with_destroy_count, default: 0 t.integer :tags_with_nullify_count, default: 0 end @@ -813,6 +824,7 @@ ActiveRecord::Schema.define do create_table :sponsors, force: true do |t| t.integer :club_id t.references :sponsorable, polymorphic: true, index: false + t.references :sponsor, polymorphic: true, index: false end create_table :string_key_objects, id: false, force: true do |t| @@ -847,6 +859,7 @@ ActiveRecord::Schema.define do t.column :taggable_type, :string t.column :taggable_id, :integer t.string :comment + t.string :type end create_table :tasks, force: true do |t| @@ -949,6 +962,7 @@ ActiveRecord::Schema.define do t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type + t.references :human, polymorphic: true, index: false end create_table :interests, force: true do |t| |