diff options
Diffstat (limited to 'activerecord')
700 files changed, 9282 insertions, 5775 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8d900d9669..167bd8c82c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,447 @@ +* Fixed inconsistency with `first(n)` when used with `limit()`. + The `first(n)` finder now respects the `limit()`, making it consistent + with `relation.to_a.first(n)`, and also with the behavior of `last(n)`. + + Fixes #23979. + + *Brian Christian* + +* Use `count(:all)` in `HasManyAssociation#count_records` to prevent invalid + SQL queries for association counting. + + *Klas Eskilson* + +* Fix to invoke callbacks when using `update_attribute`. + + *Mike Busch* + +* Fix `count(:all)` to correctly work `distinct` with custom SELECT list. + + *Ryuta Kamizono* + +* Using subselect for `delete_all` with `limit` or `offset`. + + *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* + +* Fix conflicts `counter_cache` with `touch: true` by optimistic locking. + + ``` + # create_table :posts do |t| + # t.integer :comments_count, default: 0 + # t.integer :lock_version + # t.timestamps + # end + class Post < ApplicationRecord + end + + # create_table :comments do |t| + # t.belongs_to :post + # end + class Comment < ApplicationRecord + belongs_to :post, touch: true, counter_cache: true + end + ``` + + 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 + + comment = Comment.create!(post: post) + # => begin transaction + INSERT INTO "comments" ("post_id") VALUES (1) + + UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1, + "lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1 + + 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. + + Comment.take.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 WHERE "posts"."id" = 1 + + 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. + ``` + + 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 + + comment = Comment.create!(post: post) + # => begin transaction + INSERT INTO "comments" ("post_id") VALUES (1) + + 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`. + + 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* + +* Remove deprecated arguments from `#verify!`. + + *Rafael Mendonça França* + +* Remove deprecated argument `name` from `#indexes`. + + *Rafael Mendonça França* + +* Remove deprecated method `ActiveRecord::Migrator.schema_migrations_table_name`. + + *Rafael Mendonça França* + +* Remove deprecated method `supports_primary_key?`. + + *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* + +* Automatically guess the inverse associations for STI. + + *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. @@ -6,7 +450,7 @@ * 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 + 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 @@ -72,7 +516,7 @@ *Kir Shatrov* -* Prevent making bind param if casted value is nil. +* Prevent creation of bind param if casted value is nil. *Ryuta Kamizono* @@ -109,10 +553,6 @@ *Ryuta Kamizono* -* Quote database name in `db:create` grant statement (when database user does not have access to create the database). - - *Rune Philosof* - * Raise error `UnknownMigrationVersionError` on the movement of migrations when the current migration does not exist. @@ -133,7 +573,7 @@ *bogdanvlviv* -* When calling the dynamic fixture accessor method with no arguments it now returns all fixtures of this type. +* 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* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index f9e4444f07..cce00cbc3a 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index cfbee4d6f7..19650b82ae 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products", which might look like this: CREATE TABLE products ( - id int NOT NULL auto_increment, + id bigint NOT NULL auto_increment, name varchar(255), PRIMARY KEY (id) ); @@ -162,7 +162,7 @@ This would also define the following accessors: <tt>Product#name</tt> and == Philosophy Active Record is an implementation of the object-relational mapping (ORM) -pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same +pattern[https://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same name described by Martin Fowler: "An object that wraps a row in a database table or view, @@ -199,7 +199,7 @@ Source code can be downloaded as part of the Rails project on GitHub: Active Record is released under the MIT license: -* http://www.opensource.org/licenses/MIT +* https://opensource.org/licenses/MIT == Support @@ -208,7 +208,7 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index cd22f76d01..60561e2c0f 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -43,5 +43,9 @@ You can override the +connections:+ parameter in either file using the +ARCONN+ $ ARCONN=postgresql bundle exec ruby -Itest test/cases/base_test.rb +Or + + $ bundle exec rake test:postgresql TEST=test/cases/base_test.rb + You can specify a custom location for the config file using the +ARCONFIG+ environment variable. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index fe5f9d1071..591c451da5 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rake/testtask" require_relative "test/config" @@ -66,7 +68,7 @@ end (Dir["test/cases/**/*_test.rb"].reject { |x| x.include?("/adapters/") } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(Gem.ruby, "-w" , "-Itest", file) + sh(Gem.ruby, "-w", "-Itest", file) end || raise("Failures") end end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index a626a1f21b..8e42a11df4 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| @@ -29,5 +31,5 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "activemodel", version - s.add_dependency "arel", "~> 8.0" + s.add_dependency "arel", ">= 9.0" end diff --git a/activerecord/bin/test b/activerecord/bin/test index ab69f4f603..83c192531e 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true COMPONENT_ROOT = File.expand_path("..", __dir__) require_relative "../../tools/test" diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 3257dd4ad7..1a2c78f39b 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record" require "benchmark/ips" diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb index c3648fee48..280b786d73 100644 --- a/activerecord/examples/simple.rb +++ b/activerecord/examples/simple.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record" class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 25d5e87317..b4377ad6be 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,14 +27,14 @@ require "active_support" require "active_support/rails" require "active_model" require "arel" +require "yaml" -require_relative "active_record/version" -require_relative "active_record/attribute_set" +require "active_record/version" +require "active_model/attribute_set" module ActiveRecord extend ActiveSupport::Autoload - autoload :Attribute autoload :Base autoload :Callbacks autoload :Core @@ -102,6 +104,7 @@ module ActiveRecord autoload :Result autoload :TableMetadata + autoload :Type end module Coders @@ -179,3 +182,7 @@ end ActiveSupport.on_load(:i18n) do I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__) end + +YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = "ActiveModel::AttributeSet" +YAML.load_tags["!ruby/object:ActiveRecord::Attribute::FromDatabase"] = "ActiveModel::Attribute::FromDatabase" +YAML.load_tags["!ruby/object:ActiveRecord::LazyAttributeHash"] = "ActiveModel::LazyAttributeHash" diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 10cbd5429c..e5e89734d2 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # See ActiveRecord::Aggregations::ClassMethods for documentation module Aggregations diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index de2d03cd0b..2b0b2864bc 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class AssociationRelation < Relation def initialize(klass, table, predicate_builder, association) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e782196ce6..661605d3e5 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require "active_support/core_ext/enumerable" require "active_support/core_ext/string/conversions" require "active_support/core_ext/module/remove_method" -require_relative "errors" +require "active_record/errors" module ActiveRecord class AssociationNotFoundError < ConfigurationError #:nodoc: @@ -138,26 +140,6 @@ module ActiveRecord class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: end - class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner = nil, reflection = nil) - if owner && reflection - super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") - else - super("Cannot associate new records.") - end - end - end - - class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner = nil, reflection = nil) - if owner && reflection - super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") - else - super("Cannot dissociate new records.") - end - end - end - class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner = nil, reflection = nil) if owner && reflection @@ -187,16 +169,6 @@ module ActiveRecord end end - class ReadOnlyAssociation < ActiveRecordError #:nodoc: - def initialize(reflection = nil) - if reflection - super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") - else - super("Read-only reflection error.") - end - end - end - # This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project @@ -222,13 +194,6 @@ module ActiveRecord autoload :CollectionAssociation autoload :ForeignAssociation autoload :CollectionProxy - - autoload :BelongsToAssociation - autoload :BelongsToPolymorphicAssociation - autoload :HasManyAssociation - autoload :HasManyThroughAssociation - autoload :HasOneAssociation - autoload :HasOneThroughAssociation autoload :ThroughAssociation module Builder #:nodoc: @@ -243,6 +208,13 @@ module ActiveRecord end eager_autoload do + autoload :BelongsToAssociation + autoload :BelongsToPolymorphicAssociation + autoload :HasManyAssociation + autoload :HasManyThroughAssociation + autoload :HasOneAssociation + autoload :HasOneThroughAssociation + autoload :Preloader autoload :JoinDependency autoload :AssociationScope @@ -347,6 +319,7 @@ module ActiveRecord # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X # create_other!(attributes={}) | X | | X + # reload_other | X | X | X # # === Collection associations (one-to-many / many-to-many) # | | | has_many @@ -376,6 +349,7 @@ module ActiveRecord # others.exists? | X | X | X # others.distinct | X | X | X # others.reset | X | X | X + # others.reload | X | X | X # # === Overriding generated methods # @@ -479,14 +453,14 @@ module ActiveRecord # The tables for these classes could look something like: # # CREATE TABLE users ( - # id int NOT NULL auto_increment, - # account_id int default NULL, + # id bigint NOT NULL auto_increment, + # account_id bigint default NULL, # name varchar default NULL, # PRIMARY KEY (id) # ) # # CREATE TABLE accounts ( - # id int NOT NULL auto_increment, + # id bigint NOT NULL auto_increment, # name varchar default NULL, # PRIMARY KEY (id) # ) @@ -553,9 +527,8 @@ module ActiveRecord # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' # end # - # Note: Joining, eager loading and preloading of these associations is not fully possible. + # Note: Joining, eager loading and preloading of these associations is not possible. # These operations happen before instance creation and the scope will be called with a +nil+ argument. - # This can lead to unexpected behavior and is deprecated. # # == Association callbacks # @@ -846,7 +819,7 @@ module ActiveRecord # project.milestones # fetches milestones from the database # project.milestones.size # uses the milestone cache # project.milestones.empty? # uses the milestone cache - # project.milestones(true).size # fetches milestones from the database + # project.milestones.reload.size # fetches milestones from the database # project.milestones # uses the milestone cache # # == Eager loading of associations @@ -1188,8 +1161,8 @@ module ActiveRecord # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # # [collection] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. + # Returns a Relation of all the associated objects. + # An empty Relation is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update SQL without waiting for the save or update call on the @@ -1246,6 +1219,9 @@ module ActiveRecord # [collection.create!(attributes = {})] # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [collection.reload] + # Returns a Relation of all of the associated objects, forcing a database read. + # An empty Relation is returned if none are found. # # === Example # @@ -1265,6 +1241,7 @@ module ActiveRecord # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) + # * <tt>Firm#clients.reload</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Scopes @@ -1392,7 +1369,7 @@ module ActiveRecord # has_many :tags, as: :taggable # has_many :reports, -> { readonly } # has_many :subscribers, through: :subscriptions, source: :user - def has_many(name, scope = nil, options = {}, &extension) + def has_many(name, scope = nil, **options, &extension) reflection = Builder::HasMany.build(self, name, scope, options, &extension) Reflection.add_reflection self, name, reflection end @@ -1424,6 +1401,8 @@ module ActiveRecord # [create_association!(attributes = {})] # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [reload_association] + # Returns the associated object, forcing a database read. # # === Example # @@ -1433,6 +1412,7 @@ module ActiveRecord # * <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 # @@ -1523,7 +1503,7 @@ module ActiveRecord # has_one :club, through: :membership # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable # has_one :credit_card, required: true - def has_one(name, scope = nil, options = {}) + def has_one(name, scope = nil, **options) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection end @@ -1553,6 +1533,8 @@ module ActiveRecord # [create_association!(attributes = {})] # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [reload_association] + # Returns the associated object, forcing a database read. # # === Example # @@ -1562,6 +1544,7 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) + # * <tt>Post#reload_author</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Scopes @@ -1664,7 +1647,7 @@ module ActiveRecord # belongs_to :company, touch: :employees_last_updated_at # belongs_to :user, optional: true # belongs_to :account, default: -> { company.account } - def belongs_to(name, scope = nil, options = {}) + def belongs_to(name, scope = nil, **options) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection end @@ -1702,8 +1685,8 @@ module ActiveRecord # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # # [collection] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. + # Returns a Relation of all the associated objects. + # An empty Relation is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). @@ -1741,6 +1724,9 @@ module ActiveRecord # Returns a new object of the collection type that has been instantiated # with +attributes+, linked to this object through the join table, and that has already been # saved (if it passed the validation). + # [collection.reload] + # Returns a Relation of all of the associated objects, forcing a database read. + # An empty Relation is returned if none are found. # # === Example # @@ -1759,6 +1745,7 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) + # * <tt>Developer#projects.reload</tt> # The declaration may include an +options+ hash to specialize the behavior of the association. # # === Scopes @@ -1830,7 +1817,7 @@ module ActiveRecord builder = Builder::HasAndBelongsToMany.new name, self, options - join_model = ActiveSupport::Deprecation.silence { builder.through_model } + join_model = builder.through_model const_set join_model.name, join_model private_constant join_model.name @@ -1859,7 +1846,7 @@ module ActiveRecord hm_options[k] = options[k] if options.key? k end - ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } + has_many name, scope, hm_options, &extension _reflections[name.to_s].parent_reflection = habtm_reflection end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 104de4f69d..14881cfe17 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -1,39 +1,39 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/conversions" module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - def self.create(connection, initial_table) - aliases = Hash.new(0) - aliases[initial_table] = 1 - new(connection, aliases) - end - - def self.create_with_joins(connection, initial_table, joins) + def self.create(connection, initial_table, joins) if joins.empty? - create(connection, initial_table) + aliases = Hash.new(0) else aliases = Hash.new { |h, k| h[k] = initial_count_for(connection, k, joins) } - aliases[initial_table] = 1 - new(connection, aliases) end + aliases[initial_table] = 1 + new(connection, aliases) end def self.initial_count_for(connection, name, table_joins) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = connection.quote_table_name(name).downcase + quoted_name = nil counts = table_joins.map do |join| if join.is_a?(Arel::Nodes::StringJoin) + # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase + quoted_name ||= connection.quote_table_name(name) + # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + join.left.scan( + /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size elsif join.respond_to? :left - join.left.table_name == name ? 1 : 0 + join.left.name == name ? 1 : 0 + elsif join.is_a?(Hash) + join.fetch(name, 0) else # this branch is reached by two tests: # @@ -77,10 +77,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 - attr_reader :aliases + attr_reader :aliases private diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 1138ae3462..ca1f9f1650 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/wrap" module ActiveRecord @@ -128,8 +130,8 @@ module ActiveRecord def extensions extensions = klass.default_extensions | reflection.extensions - if scope = reflection.scope - extensions |= klass.unscoped.instance_exec(owner, &scope).extensions + if reflection.scope + extensions |= reflection.scope_for(klass.unscoped, owner).extensions end extensions @@ -171,8 +173,8 @@ module ActiveRecord skip_assign = [reflection.foreign_key, reflection.type].compact assigned_keys = record.changed_attribute_names_to_save assigned_keys += except_from_scope_attributes.keys.map(&:to_s) - attributes = create_scope.except(*(assigned_keys - skip_assign)) - record.assign_attributes(attributes) + attributes = scope_for_create.except!(*(assigned_keys - skip_assign)) + record.send(:_assign_attributes, attributes) if attributes.any? set_inverse_instance(record) end @@ -185,6 +187,9 @@ module ActiveRecord end private + def scope_for_create + scope.scope_for_create + end def find_target? !loaded? && (!owner.new_record? || foreign_key_present?) && klass diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 6ef225b725..11967e0571 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class AssociationScope #:nodoc: @@ -21,15 +23,10 @@ module ActiveRecord reflection = association.reflection scope = klass.unscoped owner = association.owner - alias_tracker = AliasTracker.create(klass.connection, klass.table_name) - chain_head, chain_tail = get_chain(reflection, association, alias_tracker) + chain = get_chain(reflection, association, scope.alias_tracker) scope.extending! reflection.extensions - add_constraints(scope, owner, reflection, chain_head, chain_tail) - end - - def join_type - Arel::Nodes::InnerJoin + add_constraints(scope, owner, chain) end def self.get_bind_values(owner, chain) @@ -57,20 +54,21 @@ module ActiveRecord private def join(table, constraint) - table.create_join(table, table.create_on(constraint), join_type) + table.create_join(table, table.create_on(constraint)) end - def last_chain_scope(scope, table, reflection, owner) + def last_chain_scope(scope, reflection, owner) join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key + table = reflection.aliased_table value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + scope = apply_scope(scope, table, key, value) if reflection.type polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + scope = apply_scope(scope, table, reflection.type, polymorphic_type) end scope @@ -80,28 +78,29 @@ module ActiveRecord value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, foreign_table, next_reflection) + def next_chain_scope(scope, reflection, next_reflection) join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key + table = reflection.aliased_table + foreign_table = next_reflection.aliased_table constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type value = transform_value(next_reflection.klass.base_class.name) - scope = scope.where(table.name => { reflection.type => value }) + scope = apply_scope(scope, table, reflection.type, value) end - scope = scope.joins(join(foreign_table, constraint)) + scope.joins!(join(foreign_table, constraint)) end class ReflectionProxy < SimpleDelegator # :nodoc: - attr_accessor :next - attr_reader :alias_name + attr_reader :aliased_table - def initialize(reflection, alias_name) + def initialize(reflection, aliased_table) super(reflection) - @alias_name = alias_name + @aliased_table = aliased_table end def all_includes; nil; end @@ -109,42 +108,33 @@ module ActiveRecord def get_chain(reflection, association, tracker) name = reflection.name - runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) - previous_reflection = runtime_reflection + chain = [Reflection::RuntimeReflection.new(reflection, association)] reflection.chain.drop(1).each do |refl| - alias_name = tracker.aliased_table_for( + aliased_table = tracker.aliased_table_for( refl.table_name, refl.alias_candidate(name), refl.klass.type_caster ) - proxy = ReflectionProxy.new(refl, alias_name) - previous_reflection.next = proxy - previous_reflection = proxy + chain << ReflectionProxy.new(refl, aliased_table) end - [runtime_reflection, previous_reflection] + chain end - def add_constraints(scope, owner, refl, chain_head, chain_tail) - owner_reflection = chain_tail - table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner) + def add_constraints(scope, owner, chain) + scope = last_chain_scope(scope, chain.last, owner) - reflection = chain_head - while reflection - table = reflection.alias_name - next_reflection = reflection.next - - unless reflection == chain_tail - foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, foreign_table, next_reflection) - end + chain.each_cons(2) do |reflection, next_reflection| + scope = next_chain_scope(scope, reflection, next_reflection) + end + chain_head = chain.first + chain.reverse_each do |reflection| # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection, table, scope_chain_item, owner) + item = eval_scope(reflection, scope_chain_item, owner) - if scope_chain_item == refl.scope + if scope_chain_item == chain_head.scope scope.merge! item.except(:where, :includes) end @@ -156,15 +146,22 @@ module ActiveRecord scope.where_clause += item.where_clause scope.order_values |= item.order_values end - - reflection = next_reflection end scope end - def eval_scope(reflection, table, scope, owner) - reflection.build_scope(table).instance_exec(owner, &scope) + def apply_scope(scope, table, key, value) + if scope.table == table + scope.where!(key => value) + else + scope.where!(table.name => { key => value }) + end + end + + def eval_scope(reflection, scope, owner) + relation = reflection.build_scope(reflection.aliased_table) + relation.instance_exec(owner, &scope) || relation end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 0e61dbfb00..bd2012df84 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Belongs To Association module Associations + # = Active Record Belongs To Association class BelongsToAssociation < SingularAssociation #:nodoc: def handle_dependency target.send(options[:dependent]) if load_target @@ -10,17 +12,20 @@ module ActiveRecord if record raise_on_type_mismatch!(record) update_counters_on_replace(record) - replace_keys(record) set_inverse_instance(record) @updated = true else decrement_counters - remove_keys end self.target = record end + def target=(record) + replace_keys(record) + super + end + def default(&block) writer(owner.instance_exec(&block)) if reader.nil? end @@ -47,9 +52,9 @@ module ActiveRecord def update_counters(by) if require_counter_update? && foreign_key_present? if target && !stale_target? - target.increment!(reflection.counter_cache_column, by) + target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - klass.update_counters(target_id, reflection.counter_cache_column => by) + klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch]) end end end @@ -76,11 +81,8 @@ module ActiveRecord end def replace_keys(record) - owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) - end - - def remove_keys - owner[reflection.foreign_key] = nil + owner[reflection.foreign_key] = record ? + record._read_attribute(reflection.association_primary_key(record.class)) : nil end def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index b710cf6bdb..55d789c66a 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Belongs To Polymorphic Association module Associations + # = Active Record Belongs To Polymorphic Association class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: def klass type = owner[reflection.foreign_type] @@ -11,12 +13,7 @@ module ActiveRecord def replace_keys(record) super - owner[reflection.foreign_type] = record.class.base_class.name - end - - def remove_keys - super - owner[reflection.foreign_type] = nil + owner[reflection.foreign_type] = record ? record.class.base_class.name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index d0534056d9..7c69cd65ee 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This is the parent Association class which defines the variables # used by all associations. # @@ -36,11 +38,6 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - if scope.is_a?(Hash) - options = scope - scope = nil - end - validate_options(options) scope = build_scope(scope, extension) @@ -107,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}(*args) - association(:#{name}).reader(*args) + def #{name} + association(:#{name}).reader end CODE end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 2b9dd8aae8..c161454c1a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation #:nodoc: def self.macro @@ -32,9 +34,7 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column - if (@_after_create_counter_called ||= false) - @_after_create_counter_called = false - elsif (@_after_replace_counter_called ||= false) + if (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false elsif saved_change_to_attribute?(foreign_key) && !new_record? if reflection.polymorphic? @@ -114,9 +114,13 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - model.after_save callback.(:saved_changes), if: :saved_changes? - model.after_touch callback.(:changes_to_save) - model.after_destroy callback.(:changes_to_save) + unless reflection.counter_cache_column + model.after_create callback.(:saved_changes), if: :saved_changes? + model.after_destroy callback.(:changes_to_save) + end + + model.after_update callback.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) end def self.add_default_callbacks(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index c58b7d8160..35a72c3850 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,6 @@ -require_relative "../../associations" +# frozen_string_literal: true + +require "active_record/associations" module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: 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 6b71826431..1981da11a2 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: class JoinTableResolver # :nodoc: @@ -45,7 +47,7 @@ module ActiveRecord::Associations::Builder # :nodoc: habtm = JoinTableResolver.build lhs_model, association_name, options join_model = Class.new(ActiveRecord::Base) { - class << self; + class << self attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7864d4c536..5b9617bc6d 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class HasMany < CollectionAssociation #:nodoc: def self.macro diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 4de846d12b..bfb37d6eee 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class HasOne < SingularAssociation #:nodoc: def self.macro diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 7732b63af6..0a02ef4cc1 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class is inherited by the has_one and belongs_to association classes module ActiveRecord::Associations::Builder # :nodoc: diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index a49fb155ee..de8afc9ccb 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # = Active Record Association Collection @@ -50,14 +52,19 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.association_primary_key_type + primary_key = reflection.association_primary_key + pk_type = klass.type_for_attribute(primary_key.to_s) ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - records = klass.where(reflection.association_primary_key => ids).index_by do |r| - r.send(reflection.association_primary_key) + + records = klass.where(primary_key => ids).index_by do |r| + r.public_send(primary_key) end.values_at(*ids).compact + if records.size != ids.size - klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key) + found_ids = records.map { |record| record.public_send(primary_key) } + not_found_ids = ids - found_ids + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids) else replace(records) end @@ -70,23 +77,25 @@ module ActiveRecord end def find(*args) - if block_given? - load_target.find(*args) { |*block_args| yield(*block_args) } - else - if options[:inverse_of] && loaded? - args_flatten = args.flatten - raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? - result = find_by_scan(*args) - - result_size = Array(result).size - if !result || result_size != args_flatten.size - scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) - else - result - end + if options[:inverse_of] && loaded? + args_flatten = args.flatten + model = scope.klass + + if args_flatten.blank? + error_message = "Couldn't find #{model.name} without an ID" + raise RecordNotFound.new(error_message, model.name, model.primary_key, args) + end + + result = find_by_scan(*args) + + result_size = Array(result).size + if !result || result_size != args_flatten.size + scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) else - scope.find(*args) + result end + else + scope.find(*args) end end @@ -178,8 +187,6 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, options[:dependent]) end @@ -189,8 +196,6 @@ module ActiveRecord # Note that this method removes records from the database ignoring the # +:dependent+ option. def destroy(*records) - return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end @@ -302,15 +307,13 @@ module ActiveRecord return scope.to_a if skip_statement_cache?(scope) conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge!(as.scope(self)) - } + sc = reflection.association_scope_cache(conn, owner) do |params| + as = AssociationScope.create { params.bind } + target_scope.merge!(as.scope(self)) end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, klass, conn) do |record| + sc.execute(binds, conn) do |record| set_inverse_instance(record) end end @@ -374,11 +377,9 @@ module ActiveRecord end end - def create_scope - scope.scope_for_create.stringify_keys - end - def delete_or_destroy(records, method) + return if records.empty? + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } existing_records = records.reject(&:new_record?) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index d77fcaf668..8b4a48a38c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # Association proxies in Active Record are middlemen between the object that @@ -133,8 +135,9 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def find(*args, &block) - @association.find(*args, &block) + def find(*args) + return super if block_given? + @association.find(*args) end ## @@ -985,6 +988,12 @@ module ActiveRecord load_target == other end + ## + # :method: to_ary + # + # :call-seq: + # to_ary() + # # Returns a new array of objects from the collection. If the collection # hasn't been loaded, it fetches the records from the database. # @@ -1018,10 +1027,6 @@ module ActiveRecord # # #<Pet id: 5, name: "Brain", person_id: 1>, # # #<Pet id: 6, name: "Boss", person_id: 1> # # ] - def to_ary - load_target.dup - end - alias_method :to_a, :to_ary def records # :nodoc: load_target @@ -1069,7 +1074,6 @@ module ActiveRecord end # Reloads the collection from the database. Returns +self+. - # Equivalent to <tt>collection(true)</tt>. # # class Person < ActiveRecord::Base # has_many :pets @@ -1083,9 +1087,6 @@ module ActiveRecord # # person.pets.reload # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] - # - # person.pets(true) # fetches pets from the database - # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload reset_scope diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index 3ceec0ee46..40010cde03 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations module ForeignAssociation # :nodoc: def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 10ca0e47ff..cf85a87fa7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has Many Association module Associations + # = Active Record Has Many Association # This is the proxy that handles a has many association. # # If the association has a <tt>:through</tt> option further specialization @@ -61,7 +63,7 @@ module ActiveRecord count = if reflection.has_cached_counter? owner._read_attribute(reflection.counter_cache_column).to_i else - scope.count + scope.count(:all) end # If there's nothing in the database and @target has no new records 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 2fd20b4368..ac86911ad8 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has Many Through Association module Associations + # = Active Record Has Many Through Association class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociation @@ -145,14 +147,7 @@ module ActiveRecord count = scope.destroy_all.length 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", scope.bound_attributes) + 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 8458253ff8..7953b89f61 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has One Association module Associations + # = Active Record Has One Association class HasOneAssociation < SingularAssociation #:nodoc: include ForeignAssociation @@ -56,6 +58,7 @@ module ActiveRecord when :delete target.delete when :destroy + target.destroyed_by_association = reflection target.destroy when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? @@ -78,6 +81,7 @@ module ActiveRecord when :delete target.delete when :destroy + target.destroyed_by_association = reflection target.destroy else nullify_owner_attributes(target) 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 1183bdf6f4..36746f9115 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has One Through Association module Associations + # = Active Record Has One Through Association class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 04cdcb6a7f..df4bf07999 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class JoinDependency # :nodoc: @@ -33,20 +35,14 @@ module ActiveRecord end Table = Struct.new(:node, :columns) do # :nodoc: - def table - Arel::Nodes::TableAlias.new node.table, node.aliased_table_name - end - def column_aliases - t = table + t = node.table columns.map { |column| t[column.name].as Arel.sql column.alias } end end Column = Struct.new(:name, :alias) end - attr_reader :alias_tracker, :base_klass, :join_root - def self.make_tree(associations) hash = {} walk_tree associations, hash @@ -92,11 +88,11 @@ module ActiveRecord # associations # => [:appointments] # joins # => [] # - def initialize(base, associations, joins, eager_loading: true) - @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins) + def initialize(base, table, associations, alias_tracker, eager_loading: true) + @alias_tracker = alias_tracker @eager_loading = eager_loading tree = self.class.make_tree associations - @join_root = JoinBase.new base, build(tree, base) + @join_root = JoinBase.new(base, table, build(tree, base)) @join_root.children.each { |child| construct_tables! @join_root, child } end @@ -121,7 +117,7 @@ module ActiveRecord end def aliases - Aliases.new join_root.each_with_index.map { |join_part, i| + @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}" } @@ -129,7 +125,7 @@ module ActiveRecord } end - def instantiate(result_set, aliases) + def instantiate(result_set, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) seen = Hash.new { |i, object_id| @@ -152,7 +148,7 @@ module ActiveRecord message_bus.instrument("instantiation.active_record", payload) do 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) + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end @@ -160,6 +156,9 @@ module ActiveRecord parents.values end + protected + attr_reader :alias_tracker, :base_klass, :join_root + private def make_constraints(parent, child, tables, join_type) @@ -176,9 +175,9 @@ module ActiveRecord def make_join_constraints(parent, child, join_type, aliasing = false) tables = aliasing ? table_aliases_for(parent, child) : child.tables - info = make_constraints(parent, child, tables, join_type) + joins = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } + joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) @@ -226,7 +225,7 @@ module ActiveRecord raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new reflection, build(right, reflection.klass) + JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) end.compact end @@ -258,7 +257,8 @@ module ActiveRecord else model = construct_model(ar_parent, node, row, model_cache, id, aliases) - if node.reflection.scope_for(node.base_klass).readonly_value + if node.reflection.scope && + node.reflection.scope_for(node.base_klass.unscoped).readonly_value model.readonly! 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 b14ddfeeeb..221c791bf8 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,4 +1,6 @@ -require_relative "join_part" +# frozen_string_literal: true + +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations @@ -9,11 +11,12 @@ module ActiveRecord attr_accessor :tables - def initialize(reflection, children) + def initialize(reflection, children, alias_tracker) super(reflection.klass, children) - @reflection = reflection - @tables = nil + @alias_tracker = alias_tracker + @reflection = reflection + @tables = nil end def match?(other) @@ -21,11 +24,8 @@ module ActiveRecord super && reflection == other.reflection end - JoinInformation = Struct.new :joins, :binds - def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) joins = [] - binds = [] tables = tables.reverse # The chain starts with the target table, but we want to end with it here (makes @@ -39,28 +39,27 @@ module ActiveRecord joins << table.create_join(table, table.create_on(constraint), join_type) join_scope = reflection.join_scope(table, foreign_klass) + arel = join_scope.arel(alias_tracker.aliases) - if join_scope.arel.constraints.any? - binds.concat join_scope.bound_attributes - joins.concat join_scope.arel.join_sources + if arel.constraints.any? + joins.concat arel.join_sources right = joins.last.right - right.expr = right.expr.and(join_scope.arel.constraints) + right.expr = right.expr.and(arel.constraints) end # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end - JoinInformation.new joins, binds + joins end def table tables.first end - def aliased_table_name - table.table_alias || table.name - end + protected + attr_reader :alias_tracker end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index 6e0963425d..988b4e8fa2 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,20 +1,21 @@ -require_relative "join_part" +# frozen_string_literal: true + +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def match?(other) - return true if self == other - super && base_klass == other.base_klass - end + attr_reader :table - def table - base_klass.arel_table + def initialize(base_klass, table, children) + super(base_klass, children) + @table = table end - def aliased_table_name - base_klass.table_name + def match?(other) + return true if self == other + super && base_klass == other.base_klass 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 80c9fde5d1..2181f308bf 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class JoinDependency # :nodoc: @@ -36,11 +38,6 @@ module ActiveRecord raise NotImplementedError end - # The alias for the active_record's table - def aliased_table_name - raise NotImplementedError - end - def extract_record(row, column_names_with_alias) # This code is performance critical as it is called per row. # see: https://github.com/rails/rails/pull/12185 diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index a18994cec4..e1087be9b3 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # Implements the details of eager loading of Active Record associations. @@ -42,16 +44,8 @@ module ActiveRecord extend ActiveSupport::Autoload eager_autoload do - autoload :Association, "active_record/associations/preloader/association" - autoload :SingularAssociation, "active_record/associations/preloader/singular_association" - autoload :CollectionAssociation, "active_record/associations/preloader/collection_association" - autoload :ThroughAssociation, "active_record/associations/preloader/through_association" - - autoload :HasMany, "active_record/associations/preloader/has_many" - autoload :HasManyThrough, "active_record/associations/preloader/has_many_through" - autoload :HasOne, "active_record/associations/preloader/has_one" - autoload :HasOneThrough, "active_record/associations/preloader/has_one_through" - autoload :BelongsTo, "active_record/associations/preloader/belongs_to" + autoload :Association, "active_record/associations/preloader/association" + autoload :ThroughAssociation, "active_record/associations/preloader/through_association" end # Eager loads the named associations for the given Active Record record(s). @@ -89,13 +83,13 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = Array.wrap(records).compact.uniq - associations = Array.wrap(associations) + records = records.compact if records.empty? [] else - associations.flat_map { |association| + records.uniq! + Array.wrap(associations).flat_map { |association| preloaders_on association, records, preload_scope } end @@ -164,8 +158,6 @@ module ActiveRecord end class AlreadyLoaded # :nodoc: - attr_reader :owners, :reflection - def initialize(klass, owners, reflection, preload_scope) @owners = owners @reflection = reflection @@ -176,11 +168,13 @@ module ActiveRecord def preloaded_records owners.flat_map { |owner| owner.association(reflection.name).target } end + + protected + attr_reader :owners, :reflection end # Returns a class containing the logic needed to load preload the data - # and attach it to a relation. For example +Preloader::Association+ or - # +Preloader::HasManyThrough+. The class returned implements a `run` method + # and attach it to a relation. The class returned implements a `run` method # that accepts a preloader. def preloader_for(reflection, owners) if owners.first.association(reflection.name).loaded? @@ -188,13 +182,10 @@ module ActiveRecord end reflection.check_preloadable! - case reflection.macro - when :has_many - reflection.options[:through] ? HasManyThrough : HasMany - when :has_one - reflection.options[:through] ? HasOneThrough : HasOne - when :belongs_to - BelongsTo + if reflection.options[:through] + ThroughAssociation + else + Association end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 85343040db..735da152b7 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class Preloader class Association #:nodoc: - attr_reader :owners, :reflection, :preload_scope, :model, :klass attr_reader :preloaded_records def initialize(klass, owners, reflection, preload_scope) @@ -15,62 +16,61 @@ module ActiveRecord end def run(preloader) - preload(preloader) - end - - def preload(preloader) - raise NotImplementedError - end + records = load_records do |record| + owner = owners_by_key[convert_key(record[association_key_name])] + association = owner.association(reflection.name) + association.set_inverse_instance(record) + end - # The name of the key on the associated records - def association_key_name - raise NotImplementedError + owners.each do |owner| + associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || []) + end end - # The name of the key on the model which declares the association - def owner_key_name - raise NotImplementedError - end + protected + attr_reader :owners, :reflection, :preload_scope, :model, :klass private - def options - reflection.options + # The name of the key on the associated records + def association_key_name + reflection.join_primary_key(klass) end - def associated_records_by_owner(preloader) - records = load_records do |record| - owner = owners_by_key[convert_key(record[association_key_name])] - association = owner.association(reflection.name) - association.set_inverse_instance(record) - end + # The name of the key on the model which declares the association + def owner_key_name + reflection.join_foreign_key + end - owners.each_with_object({}) do |owner, result| - result[owner] = records[convert_key(owner[owner_key_name])] || [] + def associate_records_to_owner(owner, records) + association = owner.association(reflection.name) + if reflection.collection? + association.loaded! + association.target.concat(records) + else + association.target = records.first end end def owner_keys - unless defined?(@owner_keys) - @owner_keys = owners.map do |owner| - owner[owner_key_name] - end - @owner_keys.uniq! - @owner_keys.compact! - end - @owner_keys + @owner_keys ||= owners_by_key.keys end def owners_by_key unless defined?(@owners_by_key) @owners_by_key = owners.each_with_object({}) do |owner, h| - h[convert_key(owner[owner_key_name])] = owner + key = convert_key(owner[owner_key_name]) + h[key] = owner if key end end @owners_by_key end def key_conversion_required? - @key_conversion_required ||= association_key_type != owner_key_type + unless defined?(@key_conversion_required) + @key_conversion_required = (association_key_type != owner_key_type) + end + + @key_conversion_required end def convert_key(key) @@ -111,17 +111,17 @@ module ActiveRecord end def reflection_scope - @reflection_scope ||= reflection.scope_for(klass) + @reflection_scope ||= reflection.scope ? reflection.scope_for(klass.unscoped) : klass.unscoped end def build_scope - scope = klass.default_scoped + scope = klass.scope_for_association if reflection.type scope.where!(reflection.type => model.base_class.sti_name) end - scope.merge!(reflection_scope) + scope.merge!(reflection_scope) if reflection.scope scope.merge!(preload_scope) if preload_scope scope end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb deleted file mode 100644 index c20145770f..0000000000 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class BelongsTo < SingularAssociation #:nodoc: - def association_key_name - options[:primary_key] || klass && klass.primary_key - end - - def owner_key_name - reflection.foreign_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb deleted file mode 100644 index 26690bf16d..0000000000 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class CollectionAssociation < Association #:nodoc: - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, records| - association = owner.association(reflection.name) - association.loaded! - association.target.concat(records) - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb deleted file mode 100644 index 20df1cc19a..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasMany < CollectionAssociation #:nodoc: - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb deleted file mode 100644 index 2029871f39..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasManyThrough < CollectionAssociation #:nodoc: - include ThroughAssociation - - def associated_records_by_owner(preloader) - records_by_owner = super - - if reflection_scope.distinct_value - records_by_owner.each_value(&:uniq!) - end - - records_by_owner - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb deleted file mode 100644 index c4add621ca..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOne < SingularAssociation #:nodoc: - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb deleted file mode 100644 index f063f85574..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb +++ /dev/null @@ -1,9 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOneThrough < SingularAssociation #:nodoc: - include ThroughAssociation - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb deleted file mode 100644 index 5c5828262e..0000000000 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class SingularAssociation < Association #:nodoc: - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, associated_records| - record = associated_records.first - - association = owner.association(reflection.name) - association.target = record - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 0999746cd5..a6b7ab80a2 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -1,107 +1,105 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class Preloader - module ThroughAssociation #:nodoc: - def through_reflection - reflection.through_reflection - end - - def source_reflection - reflection.source_reflection - end - - def associated_records_by_owner(preloader) - through_scope = through_scope() - - preloader.preload(owners, - through_reflection.name, - through_scope) - - through_records = owners.map do |owner| - center = owner.association(through_reflection.name).target - [owner, Array(center)] - end - - reset_association(owners, through_reflection.name, through_scope) - - middle_records = through_records.flat_map(&:last) - - preloaders = preloader.preload(middle_records, - source_reflection.name, - reflection_scope) - + class ThroughAssociation < Association # :nodoc: + def run(preloader) + already_loaded = owners.first.association(through_reflection.name).loaded? + through_scope = through_scope() + reflection_scope = target_reflection_scope + through_preloaders = preloader.preload(owners, through_reflection.name, through_scope) + middle_records = through_preloaders.flat_map(&:preloaded_records) + preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope) @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl, h| - pl.owners.each { |middle| - h[middle] = pl - } - end - - through_records.each_with_object({}) do |(lhs, center), records_by_owner| - pl_to_middle = center.group_by { |record| middle_to_pl[record] } - - records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| - rhs_records = middles.flat_map { |r| - r.association(source_reflection.name).target - }.compact - - # Respect the order on `reflection_scope` if it exists, else use the natural order. - if reflection_scope.values[:order].present? - @id_map ||= id_to_index_map @preloaded_records - rhs_records.sort_by { |rhs| @id_map[rhs] } - else - rhs_records + owners.each do |owner| + through_records = Array(owner.association(through_reflection.name).target) + if already_loaded + if source_type = reflection.options[:source_type] + through_records = through_records.select do |record| + record[reflection.foreign_type] == source_type + end end + else + owner.association(through_reflection.name).reset if through_scope + end + result = through_records.flat_map do |record| + association = record.association(source_reflection.name) + target = association.target + association.reset if preload_scope + target end + result.compact! + if reflection_scope + result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any? + result.uniq! if reflection_scope.distinct_value + end + associate_records_to_owner(owner, result) end end private - - def id_to_index_map(ids) - id_map = {} - ids.each_with_index { |id, index| id_map[id] = index } - id_map + def through_reflection + reflection.through_reflection end - def reset_association(owners, association_name, through_scope) - should_reset = (through_scope != through_reflection.klass.unscoped) || - (options[:source_type] && through_reflection.collection?) + def source_reflection + reflection.source_reflection + end - # Don't cache the association - we would only be caching a subset - if should_reset - owners.each { |owner| - owner.association(association_name).reset - } + def preload_index + @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index| + result[id] = index end end def through_scope scope = through_reflection.klass.unscoped - values = reflection_scope.values + options = reflection.options if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] - else - unless reflection_scope.where_clause.empty? - scope.includes_values = Array(values[:includes] || options[:source]) - scope.where_clause = reflection_scope.where_clause - if joins = values[:joins] - scope.joins!(source_reflection.name => joins) - end - if left_outer_joins = values[:left_outer_joins] - scope.left_outer_joins!(source_reflection.name => left_outer_joins) - end + elsif !reflection_scope.where_clause.empty? + scope.where_clause = reflection_scope.where_clause + values = reflection_scope.values + + if includes = values[:includes] + scope.includes!(source_reflection.name => includes) + else + scope.includes!(source_reflection.name) + end + + if values[:references] && !values[:references].empty? + scope.references!(values[:references]) + else + scope.references!(source_reflection.table_name) + end + + if joins = values[:joins] + scope.joins!(source_reflection.name => joins) + end + + if left_outer_joins = values[:left_outer_joins] + scope.left_outer_joins!(source_reflection.name => left_outer_joins) end - scope.references! values[:references] if scope.eager_loading? && order_values = values[:order] scope = scope.order(order_values) end end - scope + scope unless scope.empty_scope? + end + + def target_reflection_scope + if preload_scope + reflection_scope.merge(preload_scope) + elsif reflection.scope + reflection_scope + else + nil + end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index f8bbe4c2ed..441bd715e4 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: @@ -30,9 +32,8 @@ module ActiveRecord end private - - def create_scope - scope.scope_for_create.stringify_keys.except(klass.primary_key) + def scope_for_create + super.except!(klass.primary_key) end def find_target @@ -40,15 +41,13 @@ module ActiveRecord return scope.take if skip_statement_cache?(scope) conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge!(as.scope(self)).limit(1) - } + sc = reflection.association_scope_cache(conn, owner) do |params| + as = AssociationScope.create { params.bind } + target_scope.merge!(as.scope(self)).limit(1) end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, klass, conn) do |record| + sc.execute(binds, conn) do |record| set_inverse_instance record end.first rescue ::RangeError diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 6b87993ba3..249eb1775b 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Through Association module Associations + # = Active Record Through Association module ThroughAssociation #:nodoc: delegate :source_reflection, :through_reflection, to: :reflection @@ -13,7 +15,7 @@ module ActiveRecord def target_scope scope = super reflection.chain.drop(1).each do |reflection| - relation = reflection.klass.all + relation = reflection.klass.scope_for_association scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) @@ -36,24 +38,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 diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb deleted file mode 100644 index 78662433eb..0000000000 --- a/activerecord/lib/active_record/attribute.rb +++ /dev/null @@ -1,240 +0,0 @@ -module ActiveRecord - class Attribute # :nodoc: - class << self - def from_database(name, value, type) - FromDatabase.new(name, value, type) - end - - def from_user(name, value, type, original_attribute = nil) - FromUser.new(name, value, type, original_attribute) - end - - def with_cast_value(name, value, type) - WithCastValue.new(name, value, type) - end - - def null(name) - Null.new(name) - end - - def uninitialized(name, type) - Uninitialized.new(name, type) - end - end - - attr_reader :name, :value_before_type_cast, :type - - # This method should not be called directly. - # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type, original_attribute = nil) - @name = name - @value_before_type_cast = value_before_type_cast - @type = type - @original_attribute = original_attribute - end - - def value - # `defined?` is cheaper than `||=` when we get back falsy values - @value = type_cast(value_before_type_cast) unless defined?(@value) - @value - end - - def original_value - if assigned? - original_attribute.original_value - else - type_cast(value_before_type_cast) - end - end - - def value_for_database - type.serialize(value) - end - - def changed? - changed_from_assignment? || changed_in_place? - end - - def changed_in_place? - has_been_read? && type.changed_in_place?(original_value_for_database, value) - end - - def forgetting_assignment - with_value_from_database(value_for_database) - end - - def with_value_from_user(value) - type.assert_valid_value(value) - self.class.from_user(name, value, type, original_attribute || self) - end - - def with_value_from_database(value) - self.class.from_database(name, value, type) - end - - def with_cast_value(value) - self.class.with_cast_value(name, value, type) - end - - def with_type(type) - if changed_in_place? - with_value_from_user(value).with_type(type) - else - self.class.new(name, value_before_type_cast, type, original_attribute) - end - end - - def type_cast(*) - raise NotImplementedError - end - - def initialized? - true - end - - def came_from_user? - false - end - - def has_been_read? - defined?(@value) - end - - def ==(other) - self.class == other.class && - name == other.name && - value_before_type_cast == other.value_before_type_cast && - type == other.type - end - alias eql? == - - def hash - [self.class, name, value_before_type_cast, type].hash - end - - def init_with(coder) - @name = coder["name"] - @value_before_type_cast = coder["value_before_type_cast"] - @type = coder["type"] - @original_attribute = coder["original_attribute"] - @value = coder["value"] if coder.map.key?("value") - end - - def encode_with(coder) - coder["name"] = name - coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil? - coder["type"] = type if type - coder["original_attribute"] = original_attribute if original_attribute - coder["value"] = value if defined?(@value) - 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 :original_attribute - alias_method :assigned?, :original_attribute - - def original_value_for_database - if assigned? - original_attribute.original_value_for_database - else - _original_value_for_database - end - end - - private - def initialize_dup(other) - if defined?(@value) && @value.duplicable? - @value = @value.dup - end - end - - def changed_from_assignment? - assigned? && type.changed?(original_value, value, value_before_type_cast) - end - - def _original_value_for_database - type.serialize(original_value) - end - - class FromDatabase < Attribute # :nodoc: - def type_cast(value) - type.deserialize(value) - end - - def _original_value_for_database - value_before_type_cast - end - end - - class FromUser < Attribute # :nodoc: - def type_cast(value) - type.cast(value) - end - - def came_from_user? - true - end - end - - class WithCastValue < Attribute # :nodoc: - def type_cast(value) - value - end - - def changed_in_place? - false - end - end - - class Null < Attribute # :nodoc: - def initialize(name) - super(name, nil, Type.default_value) - end - - def type_cast(*) - nil - end - - def with_type(type) - self.class.with_cast_value(name, nil, type) - end - - def with_value_from_database(value) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" - end - alias_method :with_value_from_user, :with_value_from_database - end - - class Uninitialized < Attribute # :nodoc: - UNINITIALIZED_ORIGINAL_VALUE = Object.new - - def initialize(name, type) - super(name, nil, type) - end - - def value - if block_given? - yield name - end - end - - def original_value - UNINITIALIZED_ORIGINAL_VALUE - end - - def value_for_database - end - - def initialized? - false - end - - def with_type(type) - self.class.new(name, type) - end - end - private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue - end -end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb deleted file mode 100644 index c4e731fb28..0000000000 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ /dev/null @@ -1,30 +0,0 @@ -require_relative "../attribute" - -module ActiveRecord - class Attribute # :nodoc: - class UserProvidedDefault < FromUser # :nodoc: - def initialize(name, value, type, database_default) - @user_provided_value = value - super(name, value, type, database_default) - end - - def value_before_type_cast - if user_provided_value.is_a?(Proc) - @memoized_value_before_type_cast ||= user_provided_value.call - else - @user_provided_value - end - end - - def with_type(type) - self.class.new(name, user_provided_value, type, original_attribute) - 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 :user_provided_value - end - end -end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index d0dfca0cac..8b0d9aab01 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/forbidden_attributes_protection" module ActiveRecord diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 5bc8527745..98b7805c0a 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeDecorators # :nodoc: extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 83c61fad19..64f81ca582 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,6 @@ -require "active_support/core_ext/enumerable" -require "active_support/core_ext/string/filters" +# frozen_string_literal: true + require "mutex_m" -require "concurrent/map" module ActiveRecord # = Active Record Attribute Methods @@ -34,7 +33,9 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class GeneratedAttributeMethods < Module; end # :nodoc: + class GeneratedAttributeMethods < Module #:nodoc: + include Mutex_m + end module ClassMethods def inherited(child_class) #:nodoc: @@ -43,7 +44,7 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m } + @generated_attribute_methods = GeneratedAttributeMethods.new @attribute_methods_generated = false include @generated_attribute_methods @@ -166,6 +167,46 @@ module ActiveRecord end end + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{column_name}" + COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{table_name}.#{column_name} #{direction}" + # "#{column_name}" + # "#{column_name} #{direction}" + COLUMN_NAME_ORDER_WHITELIST = /\A(?:\w+\.)?\w+(?:\s+asc|\s+desc)?\z/i + + def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + unexpected = args.reject do |arg| + arg.kind_of?(Arel::Node) || + arg.is_a?(Arel::Nodes::SqlLiteral) || + arg.is_a?(Arel::Attributes::Attribute) || + arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + end + + return if unexpected.none? + + if allow_unsafe_raw_sql == :deprecated + ActiveSupport::Deprecation.warn( + "Dangerous query method (method whose arguments are used as raw " \ + "SQL) called with non-attribute argument(s): " \ + "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ + "arguments will be disallowed in Rails 6.0. This method should " \ + "not be called with user-provided values, such as request " \ + "parameters or model attributes. Known-safe values can be passed " \ + "by wrapping them in Arel.sql()." + ) + else + raise(ActiveRecord::UnknownAttributeReference, + "Query method called with non-attribute argument(s): " + + unexpected.map(&:inspect).join(", ") + ) + end + end + # Returns true if the given attribute exists, otherwise false. # # class Person < ActiveRecord::Base @@ -235,7 +276,7 @@ module ActiveRecord return has_attribute?(name) end - return true + true end # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+. diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 115eb1ef3f..5941f51a1a 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods # = Active Record Attribute Methods Before Type Cast diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 948249a6fd..3de6fe566d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true + require "active_support/core_ext/module/attribute_accessors" -require_relative "../attribute_mutation_tracker" module ActiveRecord module AttributeMethods - module Dirty # :nodoc: + module Dirty extend ActiveSupport::Concern include ActiveModel::Dirty @@ -16,8 +16,8 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false, default: true - after_create { changes_internally_applied } - after_update { changes_internally_applied } + after_create { changes_applied } + after_update { changes_applied } # Attribute methods for "changed in last call to save?" attribute_method_affix(prefix: "saved_change_to_", suffix: "?") @@ -29,106 +29,18 @@ module ActiveRecord attribute_method_suffix("_change_to_be_saved", "_in_database") end - # Attempts to +save+ the record and clears changed attributes if successful. - def save(*) - if status = super - changes_applied - end - status - end - - # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. - def save!(*) - super.tap do - changes_applied - end - end - # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previous_mutation_tracker = nil - clear_mutation_trackers - @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new - end - end - - def initialize_dup(other) # :nodoc: - super - @attributes = self.class._default_attributes.map do |attr| - attr.with_value_from_user(@attributes.fetch_value(attr.name)) - end - clear_mutation_trackers - end - - def changes_internally_applied # :nodoc: - @mutations_before_last_save = mutation_tracker - forget_attribute_assignments - @mutations_from_database = AttributeMutationTracker.new(@attributes) - end - - def changes_applied - @previous_mutation_tracker = mutation_tracker - @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new - clear_mutation_trackers - end - - def clear_changes_information - @previous_mutation_tracker = nil - @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new - forget_attribute_assignments - clear_mutation_trackers - end - - def raw_write_attribute(attr_name, *) - result = super - clear_attribute_change(attr_name) - result - end - - def clear_attribute_changes(attr_names) - super - attr_names.each do |attr_name| - clear_attribute_change(attr_name) - end - end - - def changed_attributes - # This should only be set by methods which will call changed_attributes - # multiple times when it is known that the computed value cannot change. - if defined?(@cached_changed_attributes) - @cached_changed_attributes - else - emit_warning_if_needed("changed_attributes", "saved_changes.transform_values(&:first)") - super.reverse_merge(mutation_tracker.changed_values).freeze - end - end - - def changes - cache_changed_attributes do - emit_warning_if_needed("changes", "saved_changes") - super - end - end - - def previous_changes - unless previous_mutation_tracker.equal?(mutations_before_last_save) - ActiveSupport::Deprecation.warn(<<-EOW.strip_heredoc) - The behavior of `previous_changes` inside of after callbacks is - deprecated without replacement. In the next release of Rails, - this method inside of `after_save` will return the changes that - were just saved. - EOW + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new + @mutations_before_last_save = nil + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new + @mutations_from_database = nil end - previous_mutation_tracker.changes - end - - def attribute_changed_in_place?(attr_name) - mutation_tracker.changed_in_place?(attr_name) end # Did this attribute change when we last saved? This method can be invoked - # as `saved_change_to_name?` instead of `saved_change_to_attribute?("name")`. + # 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. @@ -151,8 +63,8 @@ module ActiveRecord # 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 - # `saved_change_to_attribute("name")` + # This method can be invoked as +saved_change_to_name+ in 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 @@ -165,7 +77,7 @@ module ActiveRecord mutations_before_last_save.original_value(attr_name) end - # Did the last call to `save` have any changes to change? + # Did the last call to +save+ have any changes to change? def saved_changes? mutations_before_last_save.any_changes? end @@ -175,116 +87,46 @@ module ActiveRecord mutations_before_last_save.changes end - # Alias for `attribute_changed?` + # Alias for +attribute_changed?+ def will_save_change_to_attribute?(attr_name, **options) mutations_from_database.changed?(attr_name, **options) end - # Alias for `attribute_change` + # Alias for +attribute_change+ def attribute_change_to_be_saved(attr_name) mutations_from_database.change_to_attribute(attr_name) end - # Alias for `attribute_was` + # Alias for +attribute_was+ def attribute_in_database(attr_name) mutations_from_database.original_value(attr_name) end - # Alias for `changed?` + # Alias for +changed?+ def has_changes_to_save? mutations_from_database.any_changes? end - # Alias for `changes` + # Alias for +changes+ def changes_to_save mutations_from_database.changes end - # Alias for `changed` + # Alias for +changed+ def changed_attribute_names_to_save changes_to_save.keys end - # Alias for `changed_attributes` + # Alias for +changed_attributes+ def attributes_in_database changes_to_save.transform_values(&:first) end - def attribute_was(*) - emit_warning_if_needed("attribute_was", "attribute_before_last_save") - super - end - - def attribute_change(*) - emit_warning_if_needed("attribute_change", "saved_change_to_attribute") - super - end - - def attribute_changed?(*) - emit_warning_if_needed("attribute_changed?", "saved_change_to_attribute?") - super - end - - def changed?(*) - emit_warning_if_needed("changed?", "saved_changes?") - super - end - - def changed(*) - emit_warning_if_needed("changed", "saved_changes.keys") - super - end - private - - def mutation_tracker - unless defined?(@mutation_tracker) - @mutation_tracker = nil - end - @mutation_tracker ||= AttributeMutationTracker.new(@attributes) - end - - def emit_warning_if_needed(method_name, new_method_name) - unless mutation_tracker.equal?(mutations_from_database) - ActiveSupport::Deprecation.warn(<<-EOW.squish) - The behavior of `#{method_name}` inside of after callbacks will - be changing in the next version of Rails. The new return value will reflect the - behavior of calling the method after `save` returned (e.g. the opposite of what - it returns now). To maintain the current behavior, use `#{new_method_name}` - instead. - EOW - end - end - - def mutations_from_database - unless defined?(@mutations_from_database) - @mutations_from_database = nil - end - @mutations_from_database ||= mutation_tracker - end - - def changes_include?(attr_name) - super || mutation_tracker.changed?(attr_name) - end - - def clear_attribute_change(attr_name) - mutation_tracker.forget_change(attr_name) - mutations_from_database.forget_change(attr_name) - end - - def attribute_will_change!(attr_name) - super - if self.class.has_attribute?(attr_name) - mutations_from_database.force_change(attr_name) - else - ActiveSupport::Deprecation.warn(<<-EOW.squish) - #{attr_name} is not an attribute known to Active Record. - This behavior is deprecated and will be removed in the next - version of Rails. If you'd like #{attr_name} to be managed - by Active Record, add `attribute :#{attr_name}` to your class. - EOW - mutations_from_database.deprecated_force_change(attr_name) - end + def write_attribute_without_type_cast(attr_name, _) + result = super + clear_attribute_change(attr_name) + result end def _update_record(*) @@ -298,35 +140,6 @@ module ActiveRecord def keys_for_partial_write changed_attribute_names_to_save & self.class.column_names end - - def forget_attribute_assignments - @attributes = @attributes.map(&:forgetting_assignment) - end - - def clear_mutation_trackers - @mutation_tracker = nil - @mutations_from_database = nil - @mutations_before_last_save = nil - end - - def previous_mutation_tracker - @previous_mutation_tracker ||= NullMutationTracker.instance - end - - def mutations_before_last_save - @mutations_before_last_save ||= previous_mutation_tracker - end - - def cache_changed_attributes - @cached_changed_attributes = changed_attributes - yield - ensure - clear_changed_attributes_cache - end - - def clear_changed_attributes_cache - remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index b9b2acff37..d8fc046e10 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "set" module ActiveRecord @@ -15,13 +17,15 @@ module ActiveRecord # Returns the primary key value. def id sync_with_transaction_state - _read_attribute(self.class.primary_key) if self.class.primary_key + primary_key = self.class.primary_key + _read_attribute(primary_key) if primary_key end # Sets the primary key value. def id=(value) sync_with_transaction_state - write_attribute(self.class.primary_key, value) if self.class.primary_key + primary_key = self.class.primary_key + _write_attribute(primary_key, value) if primary_key end # Queries the primary key value. diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 10498f4322..6757e9b66a 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods module Query diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index fdc4bf6621..4077250583 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods module Read extend ActiveSupport::Concern - module ClassMethods + module ClassMethods # :nodoc: private # We want to generate the methods via module_eval rather than @@ -29,9 +31,11 @@ module ActiveRecord temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def #{temp_method} + #{sync_with_transaction_state} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} _read_attribute(name) { |n| missing_attribute(n, caller) } end @@ -54,7 +58,9 @@ module ActiveRecord attr_name.to_s end - name = self.class.primary_key if name == "id".freeze && self.class.primary_key + primary_key = self.class.primary_key + name = primary_key if name == "id".freeze && primary_key + sync_with_transaction_state if name == primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 4d9aff76cc..ebc2baed34 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -1,8 +1,20 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods module Serialization extend ActiveSupport::Concern + class ColumnNotSerializableError < StandardError + def initialize(name, type) + super <<-EOS.strip_heredoc + 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. + EOS + end + end + module ClassMethods # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that @@ -58,9 +70,20 @@ module ActiveRecord end decorate_attribute_type(attr_name, :serialize) do |type| + if type_incompatible_with_serialize?(type, class_name_or_coder) + raise ColumnNotSerializableError.new(attr_name, type) + end + Type::Serialized.new(type, coder) end end + + private + + def type_incompatible_with_serialize?(type, class_name) + type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON || + type.respond_to?(:type_cast_array, true) && class_name == ::Array + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 1f1efe8812..d2b7817b45 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -60,7 +62,7 @@ module ActiveRecord class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] end - module ClassMethods + module ClassMethods # :nodoc: private def inherited(subclass) diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 75c5a1a600..bb0ec6a8c3 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module AttributeMethods module Write @@ -7,17 +9,19 @@ module ActiveRecord attribute_method_suffix "=" end - module ClassMethods + module ClassMethods # :nodoc: private def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__#{safe_name}=(value) name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) + #{sync_with_transaction_state} + _write_attribute(name, value) end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= undef_method :__temp__#{safe_name}= @@ -35,21 +39,29 @@ module ActiveRecord attr_name.to_s end - name = self.class.primary_key if name == "id".freeze && self.class.primary_key - @attributes.write_from_user(name, value) - value + primary_key = self.class.primary_key + name = primary_key if name == "id".freeze && primary_key + sync_with_transaction_state if name == primary_key + _write_attribute(name, value) end - def raw_write_attribute(attr_name, value) # :nodoc: - name = attr_name.to_s - @attributes.write_cast_value(name, value) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the write_attribute API + def _write_attribute(attr_name, value) # :nodoc: + @attributes.write_from_user(attr_name.to_s, value) value end private + def write_attribute_without_type_cast(attr_name, value) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value + end + # Handle *= for method_missing. def attribute=(attribute_name, value) - write_attribute(attribute_name, value) + _write_attribute(attribute_name, value) end end end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb deleted file mode 100644 index a01a58f8a5..0000000000 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ /dev/null @@ -1,114 +0,0 @@ -module ActiveRecord - class AttributeMutationTracker # :nodoc: - OPTION_NOT_GIVEN = Object.new - - def initialize(attributes) - @attributes = attributes - @forced_changes = Set.new - @deprecated_forced_changes = Set.new - end - - def changed_values - attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| - if changed?(attr_name) - result[attr_name] = attributes[attr_name].original_value - end - end - end - - def changes - attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| - change = change_to_attribute(attr_name) - if change - result[attr_name] = change - end - end - end - - def change_to_attribute(attr_name) - attr_name = attr_name.to_s - if changed?(attr_name) - [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] - end - end - - def any_changes? - attr_names.any? { |attr| changed?(attr) } || deprecated_forced_changes.any? - end - - def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) - attr_name = attr_name.to_s - forced_changes.include?(attr_name) || - attributes[attr_name].changed? && - (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) && - (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to) - end - - def changed_in_place?(attr_name) - attributes[attr_name.to_s].changed_in_place? - end - - def forget_change(attr_name) - attr_name = attr_name.to_s - attributes[attr_name] = attributes[attr_name].forgetting_assignment - forced_changes.delete(attr_name) - end - - def original_value(attr_name) - attributes[attr_name.to_s].original_value - end - - def force_change(attr_name) - forced_changes << attr_name.to_s - end - - def deprecated_force_change(attr_name) - deprecated_forced_changes << attr_name.to_s - 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 :attributes, :forced_changes, :deprecated_forced_changes - - private - - def attr_names - attributes.keys - end - end - - class NullMutationTracker # :nodoc: - include Singleton - - def changed_values(*) - {} - end - - def changes(*) - {} - end - - def change_to_attribute(attr_name) - end - - def any_changes?(*) - false - end - - def changed?(*) - false - end - - def changed_in_place?(*) - false - end - - def forget_change(*) - end - - def original_value(*) - end - end -end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb deleted file mode 100644 index 6399e3de70..0000000000 --- a/activerecord/lib/active_record/attribute_set.rb +++ /dev/null @@ -1,113 +0,0 @@ -require_relative "attribute_set/builder" -require_relative "attribute_set/yaml_encoder" - -module ActiveRecord - class AttributeSet # :nodoc: - delegate :each_value, :fetch, to: :attributes - - def initialize(attributes) - @attributes = attributes - end - - def [](name) - attributes[name] || Attribute.null(name) - end - - def []=(name, value) - attributes[name] = value - end - - def values_before_type_cast - attributes.transform_values(&:value_before_type_cast) - end - - def to_hash - initialized_attributes.transform_values(&:value) - end - alias_method :to_h, :to_hash - - def key?(name) - attributes.key?(name) && self[name].initialized? - end - - def keys - attributes.each_key.select { |name| self[name].initialized? } - end - - 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 fetch_value(name, &block) - self[name].value(&block) - end - else - def fetch_value(name) - self[name].value { |n| yield n if block_given? } - end - end - - def write_from_database(name, value) - attributes[name] = self[name].with_value_from_database(value) - end - - def write_from_user(name, value) - attributes[name] = self[name].with_value_from_user(value) - end - - def write_cast_value(name, value) - attributes[name] = self[name].with_cast_value(value) - end - - def freeze - @attributes.freeze - super - end - - def deep_dup - self.class.allocate.tap do |copy| - copy.instance_variable_set(:@attributes, attributes.deep_dup) - end - end - - def initialize_dup(_) - @attributes = attributes.dup - super - end - - def initialize_clone(_) - @attributes = attributes.clone - super - end - - def reset(key) - if key?(key) - write_from_database(key, nil) - end - end - - def accessed - attributes.select { |_, attr| attr.has_been_read? }.keys - end - - def map(&block) - new_attributes = attributes.transform_values(&block) - AttributeSet.new(new_attributes) - end - - def ==(other) - attributes == other.attributes - 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 :attributes - - private - - def initialized_attributes - attributes.select { |_, attr| attr.initialized? } - end - end -end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb deleted file mode 100644 index abe22b9ae4..0000000000 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ /dev/null @@ -1,124 +0,0 @@ -require_relative "../attribute" - -module ActiveRecord - class AttributeSet # :nodoc: - class Builder # :nodoc: - attr_reader :types, :always_initialized, :default - - def initialize(types, always_initialized = nil, &default) - @types = types - @always_initialized = always_initialized - @default = default - end - - def build_from_database(values = {}, additional_types = {}) - if always_initialized && !values.key?(always_initialized) - values[always_initialized] = nil - end - - attributes = LazyAttributeHash.new(types, values, additional_types, &default) - AttributeSet.new(attributes) - end - end - end - - class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize - - def initialize(types, values, additional_types, &default) - @types = types - @values = values - @additional_types = additional_types - @materialized = false - @delegate_hash = {} - @default = default || proc {} - end - - def key?(key) - delegate_hash.key?(key) || values.key?(key) || types.key?(key) - end - - def [](key) - delegate_hash[key] || assign_default_value(key) - end - - def []=(key, value) - if frozen? - raise RuntimeError, "Can't modify frozen hash" - end - delegate_hash[key] = value - end - - def deep_dup - dup.tap do |copy| - copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) - end - end - - def initialize_dup(_) - @delegate_hash = Hash[delegate_hash] - super - end - - def select - keys = types.keys | values.keys | delegate_hash.keys - keys.each_with_object({}) do |key, hash| - attribute = self[key] - if yield(key, attribute) - hash[key] = attribute - end - end - end - - def ==(other) - if other.is_a?(LazyAttributeHash) - materialize == other.materialize - else - materialize == other - end - end - - def marshal_dump - materialize - end - - def marshal_load(delegate_hash) - @delegate_hash = delegate_hash - @types = {} - @values = {} - @additional_types = {} - @materialized = true - 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 :types, :values, :additional_types, :delegate_hash, :default - - def materialize - unless @materialized - values.each_key { |key| self[key] } - types.each_key { |key| self[key] } - unless frozen? - @materialized = true - end - end - delegate_hash - end - - private - - def assign_default_value(name) - type = additional_types.fetch(name, types[name]) - value_present = true - value = values.fetch(name) { value_present = false } - - if value_present - delegate_hash[name] = Attribute.from_database(name, value, type) - elsif types.key?(name) - delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) - end - end - end -end diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb deleted file mode 100644 index 899de14792..0000000000 --- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb +++ /dev/null @@ -1,41 +0,0 @@ -module ActiveRecord - class AttributeSet - # Attempts to do more intelligent YAML dumping of an - # ActiveRecord::AttributeSet to reduce the size of the resulting string - class YAMLEncoder # :nodoc: - def initialize(default_types) - @default_types = default_types - end - - def encode(attribute_set, coder) - coder["concise_attributes"] = attribute_set.each_value.map do |attr| - if attr.type.equal?(default_types[attr.name]) - attr.with_type(nil) - else - attr - end - end - end - - def decode(coder) - if coder["attributes"] - coder["attributes"] - else - attributes_hash = Hash[coder["concise_attributes"].map do |attr| - if attr.type.nil? - attr = attr.with_type(default_types[attr.name]) - end - [attr.name, attr] - end] - AttributeSet.new(attributes_hash) - 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 :default_types - end - end -end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index dde22bcdaa..35150889d9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,4 +1,6 @@ -require_relative "attribute/user_provided_default" +# frozen_string_literal: true + +require "active_model/attribute/user_provided_default" module ActiveRecord # See ActiveRecord::Attributes::ClassMethods for documentation @@ -55,7 +57,7 @@ module ActiveRecord # store_listing = StoreListing.new(price_in_cents: '10.1') # # # before - # store_listing.price_in_cents # => BigDecimal.new(10.1) + # store_listing.price_in_cents # => BigDecimal(10.1) # # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :integer @@ -248,14 +250,14 @@ module ActiveRecord if value == NO_DEFAULT_PROVIDED default_attribute = _default_attributes[name].with_type(type) elsif from_user - default_attribute = Attribute::UserProvidedDefault.new( + default_attribute = ActiveModel::Attribute::UserProvidedDefault.new( name, value, type, _default_attributes.fetch(name.to_s) { nil }, ) else - default_attribute = Attribute.from_database(name, value, type) + default_attribute = ActiveModel::Attribute.from_database(name, value, type) end _default_attributes[name] = default_attribute end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 70f0e2af8e..a1250c3835 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record Autosave Association # @@ -434,6 +436,9 @@ module ActiveRecord if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key + if inverse_reflection = reflection.inverse_of + record.association(inverse_reflection.name).loaded! + end end saved = record.save(validate: !autosave) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f0e455478a..b7ad944cec 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "yaml" require "active_support/benchmarkable" require "active_support/dependencies" @@ -13,14 +15,14 @@ require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/introspection" require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" -require_relative "attribute_decorators" -require_relative "define_callbacks" -require_relative "errors" -require_relative "log_subscriber" -require_relative "explain_subscriber" -require_relative "relation/delegation" -require_relative "attributes" -require_relative "type_caster" +require "active_record/attribute_decorators" +require "active_record/define_callbacks" +require "active_record/errors" +require "active_record/log_subscriber" +require "active_record/explain_subscriber" +require "active_record/relation/delegation" +require "active_record/attributes" +require "active_record/type_caster" module ActiveRecord #:nodoc: # = Active Record diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index eb44887e18..9dbdf845bd 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record \Callbacks # @@ -96,9 +98,9 @@ module ActiveRecord # == Types of callbacks # # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects, - # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects + # inline methods (using a proc). Method references and callback objects # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for - # creating mix-ins), and inline eval methods are deprecated. + # creating mix-ins). # # The method reference callbacks work by specifying a protected or private method available in the object, like this: # @@ -230,7 +232,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_save :log_children @@ -238,7 +240,7 @@ module ActiveRecord # # private # - # def log_chidren + # def log_children # # Child processing # end # @@ -255,7 +257,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_commit :log_children @@ -263,7 +265,7 @@ module ActiveRecord # # private # - # def log_chidren + # def log_children # # Child processing # end # diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb index cb185a881e..a69b38487e 100644 --- a/activerecord/lib/active_record/coders/json.rb +++ b/activerecord/lib/active_record/coders/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Coders # :nodoc: class JSON # :nodoc: diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9c52a31b95..11559141c7 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "yaml" module ActiveRecord diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 8b937b6703..0520591f4f 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -1,22 +1,26 @@ +# frozen_string_literal: true + module ActiveRecord module CollectionCacheKey def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: - query_signature = Digest::MD5.hexdigest(collection.to_sql) + query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql) key = "#{collection.model_name.cache_key}/query-#{query_signature}" - if collection.loaded? - size = collection.size + if collection.loaded? || collection.distinct_value + size = collection.records.size if size > 0 timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end else + if collection.eager_loading? + collection = collection.send(:apply_join_dependency) + end column_type = type_for_attribute(timestamp_column.to_s) - column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(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" - if collection.limit_value || collection.offset_value - query = collection.spawn - query.select_values = [column] + if collection.has_limit_or_offset? + query = collection.select(column) subquery_alias = "subquery_for_cache_key" subquery_column = "#{subquery_alias}.#{timestamp_column}" subquery = query.arel.as(subquery_alias) @@ -27,7 +31,7 @@ module ActiveRecord arel = query.arel end - result = connection.select_one(arel, nil, query.bound_attributes) + result = connection.select_one(arel, nil) if result.blank? size = 0 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 627b753f01..c730584902 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "thread" require "concurrent/map" require "monitor" @@ -61,15 +63,13 @@ module ActiveRecord # There are several connection-pooling-related options that you can add to # your database connection configuration: # - # * +pool+: number indicating size of connection pool (default 5) - # * +checkout_timeout+: number of seconds to block and wait for a connection - # before giving up and raising a timeout error (default 5 seconds). - # * +reaping_frequency+: frequency in seconds to periodically run the - # Reaper, which attempts to find and recover connections from dead - # threads, which can occur if a programmer forgets to close a - # connection at the end of a thread or a thread dies unexpectedly. - # Regardless of this setting, the Reaper will be invoked before every - # blocking wait. (Default +nil+, which means don't schedule the Reaper). + # * +pool+: maximum number of connections the pool may manage (default 5). + # * +idle_timeout+: number of seconds that a connection will be kept + # unused in the pool before it is automatically disconnected (default + # 300 seconds). Set this to zero to keep connections forever. + # * +checkout_timeout+: number of seconds to wait for a connection to + # become available before giving up and raising a timeout error (default + # 5 seconds). # #-- # Synchronization policy: @@ -80,11 +80,8 @@ module ActiveRecord # * private methods that require being called in a +synchronize+ blocks # are now explicitly documented class ConnectionPool - # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool - # with which it shares a Monitor. But could be a generic Queue. - # - # The Queue in stdlib's 'thread' could replace this class except - # stdlib's doesn't support waiting with a timeout. + # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. class Queue def initialize(lock = Monitor.new) @lock = lock @@ -173,7 +170,7 @@ module ActiveRecord # Removes and returns the head of the queue if possible, or +nil+. def remove - @queue.shift + @queue.pop end # Remove and return the head the queue if the number of @@ -268,7 +265,7 @@ module ActiveRecord # Connections must be leased while holding the main pool mutex. This is # an internal subclass that also +.leases+ returned connections while # still in queue's critical section (queue synchronizes with the same - # +@lock+ as the main pool) so that a returned connection is already + # <tt>@lock</tt> as the main pool) so that a returned connection is already # leased and there is no need to re-enter synchronized block. class ConnectionLeasingQueue < Queue # :nodoc: include BiasableQueue @@ -281,12 +278,12 @@ module ActiveRecord end end - # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a +nil+ frequency will never reap the - # connection pool. + # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on + # +pool+. A reaper instantiated with a zero frequency will never reap + # the connection pool. # - # Configure the frequency by setting "reaping_frequency" in your - # database yaml file. + # Configure the frequency by setting +reaping_frequency+ in your database + # yaml file (default 60 seconds). class Reaper attr_reader :pool, :frequency @@ -296,11 +293,12 @@ module ActiveRecord end def run - return unless frequency + return unless frequency && frequency > 0 Thread.new(frequency, pool) { |t, p| loop do sleep t p.reap + p.flush end } end @@ -324,8 +322,10 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 - @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) - @reaper.run + if @idle_timeout = spec.config.fetch(:idle_timeout, 300) + @idle_timeout = @idle_timeout.to_f + @idle_timeout = nil if @idle_timeout <= 0 + end # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @@ -338,7 +338,7 @@ module ActiveRecord # then that +thread+ does indeed own that +conn+. However, an absence of a such # mapping does not mean that the +thread+ doesn't own the said connection. In # that case +conn.owner+ attr should be consulted. - # Access and modification of +@thread_cached_conns+ does not require + # Access and modification of <tt>@thread_cached_conns</tt> does not require # synchronization. @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size) @@ -355,6 +355,12 @@ module ActiveRecord @available = ConnectionLeasingQueue.new self @lock_thread = false + + # +reaping_frequency+ is configurable mostly for historical reasons, but it could + # also be useful if someone wants a very low +idle_timeout+. + reaping_frequency = spec.config.fetch(:reaping_frequency, 60) + @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f) + @reaper.run end def lock_thread=(lock_thread) @@ -447,6 +453,21 @@ module ActiveRecord disconnect(false) end + # Discards all connections in the pool (even if they're currently + # leased!), along with the pool itself. Any further interaction with the + # pool (except #spec and #schema_cache) is undefined. + # + # See AbstractAdapter#discard! + def discard! # :nodoc: + synchronize do + return if @connections.nil? # already discarded + @connections.each do |conn| + conn.discard! + end + @connections = @available = @thread_cached_conns = nil + end + end + # Clears the cache which maps classes and re-connects connections that # require reloading. # @@ -572,6 +593,35 @@ module ActiveRecord end end + # Disconnect all connections that have been idle for at least + # +minimum_idle+ seconds. Connections currently checked out, or that were + # checked in less than +minimum_idle+ seconds ago, are unaffected. + def flush(minimum_idle = @idle_timeout) + return if minimum_idle.nil? + + idle_connections = synchronize do + @connections.select do |conn| + !conn.in_use? && conn.seconds_idle >= minimum_idle + end.each do |conn| + conn.lease + + @available.delete conn + @connections.delete conn + end + end + + idle_connections.each do |conn| + conn.disconnect! + end + end + + # Disconnect all currently idle connections. Connections currently checked + # out are unaffected. + def flush! + reap + flush(-1) + end + def num_waiting_in_queue # :nodoc: @available.num_waiting end @@ -734,10 +784,10 @@ module ActiveRecord # Implementation detail: the connection returned by +acquire_connection+ # will already be "+connection.lease+ -ed" to the current thread. def acquire_connection(checkout_timeout) - # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # NOTE: we rely on <tt>@available.poll</tt> and +try_to_checkout_new_connection+ to # +conn.lease+ the returned connection (and to do this in a +synchronized+ # section). This is not the cleanest implementation, as ideally we would - # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to <tt>@available.poll</tt> # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections # of the said methods and avoid an additional +synchronize+ overhead. if conn = @available.poll || try_to_checkout_new_connection @@ -761,7 +811,7 @@ module ActiveRecord end end - # If the pool is not at a +@size+ limit, establish new connection. Connecting + # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting # to the DB is done outside main synchronized section. #-- # Implementation constraint: a newly established connection returned by this @@ -863,11 +913,31 @@ module ActiveRecord # about the model. The model needs to pass a specification name to the handler, # in order to look up the correct connection pool. class ConnectionHandler + def self.unowned_pool_finalizer(pid_map) # :nodoc: + lambda do |_| + discard_unowned_pools(pid_map) + end + end + + def self.discard_unowned_pools(pid_map) # :nodoc: + pid_map.each do |pid, pools| + pools.values.compact.each(&:discard!) unless pid == Process.pid + end + end + def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| + # Discard the parent's connection pools immediately; we have no need + # of them + ConnectionHandler.discard_unowned_pools(h) + h[k] = Concurrent::Map.new(initial_capacity: 2) end + + # Backup finalizer: if the forked child never needed a pool, the above + # early discard has not occurred + ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool) end def connection_pool_list @@ -921,6 +991,13 @@ module ActiveRecord connection_pool_list.each(&:disconnect!) end + # Disconnects all currently idle connections. + # + # See ConnectionPool#flush! for details. + def flush_idle_connections! + connection_pool_list.each(&:flush!) + end + # Locate the connection of the nearest super class. This can be an # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined @@ -928,9 +1005,7 @@ module ActiveRecord def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool - conn = pool.connection - raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn - conn + pool.connection end # Returns true if a connection that's accessible to this class has diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 407e019326..7a9e7add24 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits 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 879626b72a..36048bee03 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements @@ -7,30 +9,43 @@ module ActiveRecord end # Converts an arel AST to SQL - def to_sql(arel, binds = []) - if arel.respond_to?(:ast) - collected = visitor.accept(arel.ast, collector) - collected.compile(binds, self).freeze + def to_sql(arel_or_sql_string, binds = []) + sql, _ = to_sql_and_binds(arel_or_sql_string, binds) + sql + end + + def to_sql_and_binds(arel_or_sql_string, binds = []) # :nodoc: + if arel_or_sql_string.respond_to?(:ast) + unless binds.empty? + raise "Passing bind parameters with an arel AST is forbidden. " \ + "The values must be stored on the AST directly" + end + sql, binds = visitor.accept(arel_or_sql_string.ast, collector).value + [sql.freeze, binds || []] else - arel.dup.freeze + [arel_or_sql_string.dup.freeze, binds] end end + private :to_sql_and_binds # This is used in the StatementCache object. It returns an object that # can be used to query the database repeatedly. def cacheable_query(klass, arel) # :nodoc: - collected = visitor.accept(arel.ast, collector) if prepared_statements - klass.query(collected.value) + sql, binds = visitor.accept(arel.ast, collector).value + query = klass.query(sql) else - klass.partial_query(collected.value) + collector = PartialQueryCollector.new + parts, binds = visitor.accept(arel.ast, collector).value + query = klass.partial_query(parts) end + [query, binds] end # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = [], preparable: nil) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) + arel = arel_from_relation(arel) + sql, binds = to_sql_and_binds(arel, binds) if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false else @@ -130,19 +145,22 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - value = exec_insert(to_sql(arel, binds), name, binds, pk, sequence_name) + sql, binds = to_sql_and_binds(arel, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end alias create insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) - exec_update(to_sql(arel, binds), name, binds) + sql, binds = to_sql_and_binds(arel, binds) + exec_update(sql, name, binds) end # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) - exec_delete(to_sql(arel, binds), name, binds) + sql, binds = to_sql_and_binds(arel, binds) + exec_delete(sql, name, binds) end # Returns +true+ when the connection adapter supports prepared statement @@ -163,7 +181,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html + # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -215,7 +233,7 @@ module ActiveRecord # You should consult the documentation for your database to understand the # semantics of these different levels: # - # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://www.postgresql.org/docs/current/static/transaction-iso.html # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # # An ActiveRecord::TransactionIsolationError will be raised if: @@ -428,11 +446,12 @@ module ActiveRecord row && row.first end - def binds_from_relation(relation, binds) - if relation.is_a?(Relation) && binds.empty? - relation, binds = relation.arel, relation.bound_attributes + def arel_from_relation(relation) + if relation.is_a?(Relation) + relation.arel + else + relation end - [relation, binds] end # Fixture value is quoted by Arel, however scalar values @@ -445,6 +464,28 @@ module ActiveRecord value end end + + class PartialQueryCollector + def initialize + @parts = [] + @binds = [] + end + + def <<(str) + @parts << str + self + end + + def add_bind(obj) + @binds << obj + @parts << Arel::Nodes::BindParam.new(1) + self + end + + def value + [@parts, @binds] + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index c352ddfc11..25622e34c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require "concurrent/map" + module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -90,8 +94,8 @@ module ActiveRecord def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) + arel = arel_from_relation(arel) + sql, binds = to_sql_and_binds(arel, binds) cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } else super diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 61233dcc51..92e46ccf9f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/big_decimal/conversions" require "active_support/multibyte/chars" @@ -5,21 +7,12 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module Quoting # Quotes the column value to help prevent - # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. + # {SQL injection attacks}[https://en.wikipedia.org/wiki/SQL_injection]. def quote(value) value = id_value_for_database(value) if value.is_a?(Base) - if value.respond_to?(:quoted_id) - at = value.method(:quoted_id).source_location - at &&= " at %s:%d" % at - - owner = value.method(:quoted_id).owner.to_s - klass = value.class.to_s - klass += "(#{owner})" unless owner == klass - - ActiveSupport::Deprecation.warn \ - "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})" - return value.quoted_id + if value.respond_to?(:value_for_database) + value = value.value_for_database end _quote(value) @@ -31,10 +24,6 @@ module ActiveRecord def type_cast(value, column = nil) value = id_value_for_database(value) if value.is_a?(Base) - if value.respond_to?(:quoted_id) && value.respond_to?(:id) - return value.id - end - if column value = type_cast_from_column(column, value) end @@ -106,19 +95,19 @@ module ActiveRecord end def quoted_true - "'t'".freeze + "TRUE".freeze end def unquoted_true - "t".freeze + true end def quoted_false - "'f'".freeze + "FALSE".freeze end def unquoted_false - "f".freeze + false end # Quote date/time values for use in SQL input. Includes microseconds diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index 3a06f75292..52a796b926 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module Savepoints 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 8865e7c703..4a191d337c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/strip" module ActiveRecord @@ -60,7 +62,7 @@ module ActiveRecord end def visit_PrimaryKeyDefinition(o) - "PRIMARY KEY (#{o.name.join(', ')})" + "PRIMARY KEY (#{o.name.map { |name| quote_column_name(name) }.join(', ')})" end def visit_ForeignKeyDefinition(o) @@ -93,6 +95,7 @@ module ActiveRecord if options_sql = options[:options] create_sql << " #{options_sql}" end + create_sql end def column_options(o) 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 a30fbe0e05..0594b4b485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes class IndexDefinition # :nodoc: - attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment def initialize( table, name, @@ -12,6 +14,7 @@ module ActiveRecord columns = [], lengths: {}, orders: {}, + opclasses: {}, where: nil, type: nil, using: nil, @@ -21,13 +24,23 @@ module ActiveRecord @name = name @unique = unique @columns = columns - @lengths = lengths - @orders = orders + @lengths = concise_options(lengths) + @orders = concise_options(orders) + @opclasses = concise_options(opclasses) @where = where @type = type @using = using @comment = comment end + + private + def concise_options(options) + if columns.size == options.size && options.values.uniq.size == 1 + options.values.first + else + options + end + end end # Abstract representation of a column definition. Instances of this type @@ -83,6 +96,11 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def validate? + options.fetch(:validate, true) + end + alias validated? validate? + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s @@ -146,7 +164,7 @@ module ActiveRecord end def polymorphic_options - as_options(polymorphic).merge(null: options[:null]) + as_options(polymorphic).merge(options.slice(:null, :first, :after)) end def index_options @@ -202,6 +220,7 @@ module ActiveRecord :decimal, :float, :integer, + :json, :string, :text, :time, @@ -394,6 +413,9 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, **options) # :nodoc: + if integer_like_primary_key?(type, options) + type = integer_like_primary_key_type(type, options) + end type = aliased_types(type.to_s, type) options[:primary_key] ||= type == :primary_key options[:null] = false if options[:primary_key] @@ -408,6 +430,14 @@ module ActiveRecord def aliased_types(name, fallback) "timestamp" == name ? :datetime : fallback end + + def integer_like_primary_key?(type, options) + options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default) + end + + def integer_like_primary_key_type(type, options) + type + end end class AlterTable # :nodoc: 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 34036d8a1d..1926603474 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,63 +1,40 @@ +# frozen_string_literal: true + +require "active_support/core_ext/hash/compact" + module ActiveRecord module ConnectionAdapters # :nodoc: - # The goal of this module is to move Adapter specific column - # definitions to the Adapter instead of having it in the schema - # dumper itself. This code represents the normal case. - # We can then redefine how certain data types may be handled in the schema dumper on the - # Adapter level by over-writing this code inside the database specific adapters - module ColumnDumper - def column_spec(column) - [schema_type_with_virtual(column), prepare_column_options(column)] + class SchemaDumper < SchemaDumper # :nodoc: + def self.create(connection, options) + new(connection, options) end - def column_spec_for_primary_key(column) - return {} if default_primary_key?(column) - spec = { id: schema_type(column).inspect } - spec.merge!(prepare_column_options(column).except!(:null)) - spec[:default] ||= "nil" if explicit_primary_key_default?(column) - spec - end - - # This can be overridden on an Adapter level basis to support other - # extended datatypes (Example: Adding an array option in the - # PostgreSQL::ColumnDumper) - def prepare_column_options(column) - spec = {} - - if limit = schema_limit(column) - spec[:limit] = limit + private + def column_spec(column) + [schema_type_with_virtual(column), prepare_column_options(column)] end - if precision = schema_precision(column) - spec[:precision] = precision + def column_spec_for_primary_key(column) + return {} if default_primary_key?(column) + spec = { id: schema_type(column).inspect } + spec.merge!(prepare_column_options(column).except!(:null)) + spec[:default] ||= "nil" if explicit_primary_key_default?(column) + spec end - if scale = schema_scale(column) - spec[:scale] = scale + def prepare_column_options(column) + spec = {} + spec[:limit] = schema_limit(column) + spec[:precision] = schema_precision(column) + spec[:scale] = schema_scale(column) + spec[:default] = schema_default(column) + spec[:null] = "false" unless column.null + spec[:collation] = schema_collation(column) + spec[:comment] = column.comment.inspect if column.comment.present? + spec.compact! + spec end - default = schema_default(column) if column.has_default? - spec[:default] = default unless default.nil? - - spec[:null] = "false" unless column.null - - if collation = schema_collation(column) - spec[:collation] = collation - end - - spec[:comment] = column.comment.inspect if column.comment.present? - - spec - end - - # Lists the valid migration options - def migration_keys # :nodoc: - column_options_keys - end - deprecate :migration_keys - - private - def default_primary_key?(column) schema_type(column) == :bigint end @@ -67,7 +44,7 @@ module ActiveRecord end def schema_type_with_virtual(column) - if supports_virtual_columns? && column.virtual? + if @connection.supports_virtual_columns? && column.virtual? :virtual else schema_type(column) @@ -84,7 +61,7 @@ module ActiveRecord def schema_limit(column) limit = column.limit unless column.bigint? - limit.inspect if limit && limit != native_database_types[column.type][:limit] + limit.inspect if limit && limit != @connection.native_database_types[column.type][:limit] end def schema_precision(column) @@ -96,7 +73,8 @@ module ActiveRecord end def schema_default(column) - type = lookup_cast_type_from_column(column) + return unless column.has_default? + type = @connection.lookup_cast_type_from_column(column) default = type.deserialize(column.default) if default.nil? schema_expression(column) 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 475463c4fd..4f58b0242c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,6 +1,8 @@ -require_relative "../../migration/join_table" +# frozen_string_literal: true + +require "active_record/migration/join_table" require "active_support/core_ext/string/access" -require "digest" +require "digest/sha2" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -77,7 +79,7 @@ module ActiveRecord end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) + def indexes(table_name) raise NotImplementedError, "#indexes is not implemented" end @@ -214,7 +216,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int auto_increment PRIMARY KEY + # id bigint auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -226,7 +228,7 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int auto_increment PRIMARY KEY, + # guid bigint auto_increment PRIMARY KEY, # name varchar(80) # ) # @@ -253,8 +255,8 @@ module ActiveRecord # generates: # # CREATE TABLE order ( - # product_id integer NOT NULL, - # client_id integer NOT NULL + # product_id bigint NOT NULL, + # client_id bigint NOT NULL # ); # # ALTER TABLE ONLY "orders" @@ -263,15 +265,15 @@ module ActiveRecord # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| - # t.column :category_id, :integer - # t.column :supplier_id, :integer + # t.column :category_id, :bigint + # t.column :supplier_id, :bigint # end # # generates: # # CREATE TABLE categories_suppliers ( - # category_id int, - # supplier_id int + # category_id bigint, + # supplier_id bigint # ) # # ====== Create a temporary table based on a query @@ -359,8 +361,8 @@ module ActiveRecord # generates: # # CREATE TABLE assemblies_parts ( - # assembly_id int NOT NULL, - # part_id int NOT NULL, + # assembly_id bigint NOT NULL, + # part_id bigint NOT NULL, # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # def create_join_table(table_1, table_2, column_options: {}, **options) @@ -404,6 +406,8 @@ module ActiveRecord # # Defaults to false. # + # Only supported on the MySQL adapter, ignored elsewhere. + # # ====== Add a column # # change_table(:suppliers) do |t| @@ -428,7 +432,7 @@ module ActiveRecord # t.references :company # end # - # Creates a <tt>company_id(integer)</tt> column. + # Creates a <tt>company_id(bigint)</tt> column. # # ====== Add a polymorphic foreign key column # @@ -436,7 +440,7 @@ module ActiveRecord # t.belongs_to :company, polymorphic: true # end # - # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns. + # Creates <tt>company_type(varchar)</tt> and <tt>company_id(bigint)</tt> columns. # # ====== Remove a column # @@ -509,6 +513,7 @@ module ActiveRecord # * <tt>:limit</tt> - # Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # This option is ignored by some backends. # * <tt>:default</tt> - # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - @@ -517,6 +522,8 @@ module ActiveRecord # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:comment</tt> - + # Specifies the comment for the column. This option is ignored by some backends. # # Note: The precision is the total number of significant digits, # and the scale is the number of digits that can be stored following @@ -593,7 +600,7 @@ module ActiveRecord # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. def remove_column(table_name, column_name, type = nil, options = {}) - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end # Changes the column's definition according to the new options. @@ -731,6 +738,28 @@ module ActiveRecord # # Note: only supported by PostgreSQL and MySQL # + # ====== 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 + # + # 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 + # + # 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 + # + # Note: only supported by PostgreSQL + # # ====== Creating an index with a specific type # # add_index(:developers, :name, type: :fulltext) @@ -777,7 +806,7 @@ module ActiveRecord def rename_index(table_name, old_name, new_name) validate_index_length!(table_name, new_name) - # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) + # this is a naive implementation; some DBs may support this more efficiently (PostgreSQL, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique) @@ -799,24 +828,19 @@ module ActiveRecord end # Verifies the existence of an index with a given name. - def index_name_exists?(table_name, index_name, default = nil) - unless default.nil? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing default to #index_name_exists? is deprecated without replacement. - MSG - end + def index_name_exists?(table_name, index_name) index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end - # Adds a reference. The reference column is an integer by default, + # Adds a reference. The reference column is a bigint by default, # the <tt>:type</tt> option can be used to specify a different type. # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. # #add_reference and #add_belongs_to are acceptable. # # The +options+ hash can include the following keys: # [<tt>:type</tt>] - # The reference column type. Defaults to +:integer+. + # The reference column type. Defaults to +:bigint+. # [<tt>:index</tt>] # Add an appropriate index. Defaults to true. # See #add_index for usage of this option. @@ -827,7 +851,7 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id integer column + # ====== Create a user_id bigint column # # add_reference(:products, :user) # @@ -940,6 +964,8 @@ module ActiveRecord # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ + # [<tt>:validate</tt>] + # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+. def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -1013,16 +1039,6 @@ module ActiveRecord insert_versions_sql(versions) if versions.any? end - def initialize_schema_migrations_table # :nodoc: - ActiveRecord::SchemaMigration.create_table - end - deprecate :initialize_schema_migrations_table - - def initialize_internal_metadata_table # :nodoc: - ActiveRecord::InternalMetadata.create_table - end - deprecate :initialize_internal_metadata_table - def internal_string_options_for_primary_key # :nodoc: { primary_key: true } end @@ -1128,7 +1144,7 @@ module ActiveRecord def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: column_names = index_column_names(column_name) - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" @@ -1167,30 +1183,36 @@ module ActiveRecord end # Changes the comment for a column or removes it if +nil+. - def change_column_comment(table_name, column_name, comment) #:nodoc: + def change_column_comment(table_name, column_name, comment) raise NotImplementedError, "#{self.class} does not support changing column comments" end + def create_schema_dumper(options) # :nodoc: + SchemaDumper.create(self, options) + end + private def column_options_keys [:limit, :precision, :scale, :default, :null, :collation, :comment] end def add_index_sort_order(quoted_columns, **options) - if order = options[:order] - case order - when Hash - order = order.symbolize_keys - quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } - when String - quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } - end + orders = options_for_index_columns(options[:order]) + quoted_columns.each do |name, column| + column << " #{orders[name].upcase}" if orders[name].present? end + end - quoted_columns + def options_for_index_columns(options) + if options.is_a?(Hash) + options.symbolize_keys + else + Hash.new { |hash, column| hash[column] = options } + end end - # Overridden by the MySQL adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths and by + # the PostgreSQL adapter for supporting operator classes. def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? quoted_columns = add_index_sort_order(quoted_columns, options) @@ -1344,6 +1366,20 @@ module ActiveRecord options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + def add_column_for_alter(table_name, column_name, type, options = {}) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.accept(AddColumnDefinition.new(cd)) + end + + def remove_column_for_alter(table_name, column_name, type = nil, options = {}) + "DROP COLUMN #{quote_column_name(column_name)}" + end + + def remove_columns_for_alter(table_name, *column_names) + column_names.map { |column_name| remove_column_for_alter(table_name, column_name) } + end + def insert_versions_sql(versions) sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index f63d09039f..d9ac8db6a8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters class TransactionState @@ -238,7 +240,7 @@ module ActiveRecord rollback_transaction if transaction else begin - commit_transaction + commit_transaction if transaction rescue Exception rollback_transaction(transaction) unless transaction.state.completed? raise diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cfe1892d78..2655407d75 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,11 +1,15 @@ -require_relative "../type" -require_relative "determine_if_preparable_visitor" -require_relative "schema_cache" -require_relative "sql_type_metadata" -require_relative "abstract/schema_dumper" -require_relative "abstract/schema_creation" +# frozen_string_literal: true + +require "active_record/connection_adapters/determine_if_preparable_visitor" +require "active_record/connection_adapters/schema_cache" +require "active_record/connection_adapters/sql_type_metadata" +require "active_record/connection_adapters/abstract/schema_dumper" +require "active_record/connection_adapters/abstract/schema_creation" +require "active_support/concurrency/load_interlock_aware_monitor" require "arel/collectors/bind" +require "arel/collectors/composite" require "arel/collectors/sql_string" +require "arel/collectors/substitute_binds" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -68,7 +72,6 @@ module ActiveRecord include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache - include ColumnDumper include Savepoints SIMPLE_INT = /\A\d+\z/ @@ -102,10 +105,11 @@ module ActiveRecord @logger = logger @config = config @pool = nil + @idle_since = Concurrent.monotonic_time @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} @visitor = arel_visitor - @lock = Monitor.new + @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -127,19 +131,6 @@ module ActiveRecord end end - class BindCollector < Arel::Collectors::Bind - def compile(bvs, conn) - casted_binds = bvs.map(&:value_for_database) - super(casted_binds.map { |value| conn.quote(value) }) - end - end - - class SQLString < Arel::Collectors::SQLString - def compile(bvs, conn) - super(bvs) - end - end - def valid_type?(type) # :nodoc: !native_database_types[type].nil? end @@ -174,6 +165,7 @@ module ActiveRecord "Current thread: #{Thread.current}." end + @idle_since = Concurrent.monotonic_time @owner = nil else raise ActiveRecordError, "Cannot expire connection, it is not currently leased." @@ -193,6 +185,12 @@ module ActiveRecord end end + # Seconds since this connection was returned to the pool + def seconds_idle # :nodoc: + return 0 if in_use? + Concurrent.monotonic_time - @idle_since + end + def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false yield @@ -206,16 +204,6 @@ module ActiveRecord self.class::ADAPTER_NAME end - def supports_migrations? # :nodoc: - true - end - deprecate :supports_migrations? - - def supports_primary_key? # :nodoc: - true - end - deprecate :supports_primary_key? - # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? def supports_ddl_transactions? @@ -284,6 +272,11 @@ module ActiveRecord false end + # Does this adapter support creating invalid constraints? + def supports_validate_constraints? + false + end + # Does this adapter support creating foreign key constraints # in the same statement as creating the table? def supports_foreign_keys_in_create? @@ -387,6 +380,19 @@ module ActiveRecord reset_transaction end + # Immediately forget this connection ever existed. Unlike disconnect!, + # this will not communicate with the server. + # + # After calling this method, the behavior of all other methods becomes + # undefined. This is called internally just before a forked process gets + # rid of a connection that belonged to its parent. + def discard! + # This should be overridden by concrete adapters. + # + # Prevent @connection's finalizer from touching the socket, or + # otherwise communicating with its server, when it is collected. + end + # Reset the state of this connection, directing the DBMS to clear # transactions and other connection-related server-side state. Usually a # database-dependent operation. @@ -412,10 +418,7 @@ module ActiveRecord # Checks whether the connection to the database is still active (i.e. not stale). # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. - def verify!(*ignored) - if ignored.size > 0 - ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.") - end + def verify! reconnect! unless active? end @@ -451,32 +454,12 @@ module ActiveRecord pool.checkin self end - def type_map # :nodoc: - @type_map ||= Type::TypeMap.new.tap do |mapping| - initialize_type_map(mapping) - end + def column_name_for_operation(operation, node) # :nodoc: + column_name_from_arel_node(node) end - def column_name_for_operation(operation, node) # :nodoc: - visitor.accept(node, collector).value - end - - def combine_bind_parameters( - from_clause: [], - join_clause: [], - where_clause: [], - having_clause: [], - limit: nil, - offset: nil - ) # :nodoc: - result = from_clause + join_clause + where_clause + having_clause - if limit - result << limit - end - if offset - result << offset - end - result + def column_name_from_arel_node(node) # :nodoc: + visitor.accept(node, Arel::Collectors::SQLString.new).value end def default_index_type?(index) # :nodoc: @@ -484,8 +467,13 @@ module ActiveRecord end private + def type_map + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end - def initialize_type_map(m) + def initialize_type_map(m = type_map) register_class_with_limit m, %r(boolean)i, Type::Boolean register_class_with_limit m, %r(char)i, Type::String register_class_with_limit m, %r(binary)i, Type::Binary @@ -503,6 +491,8 @@ module ActiveRecord m.alias_type %r(number)i, "decimal" m.alias_type %r(double)i, "float" + m.register_type %r(^json)i, Type::Json.new + m.register_type(%r(decimal)i) do |sql_type| scale = extract_scale(sql_type) precision = extract_precision(sql_type) @@ -518,7 +508,7 @@ module ActiveRecord def reload_type_map type_map.clear - initialize_type_map(type_map) + initialize_type_map end def register_class_with_limit(mapping, key, klass) @@ -547,12 +537,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) @@ -576,12 +561,14 @@ module ActiveRecord type_casted_binds: type_casted_binds, statement_name: statement_name, connection_id: object_id) do + begin @lock.synchronize do yield end + rescue => e + raise translate_exception_class(e, sql) end - rescue => e - raise translate_exception_class(e, sql) + end end def translate_exception(exception, message) @@ -606,9 +593,15 @@ module ActiveRecord def collector if prepared_statements - SQLString.new + Arel::Collectors::Composite.new( + Arel::Collectors::SQLString.new, + Arel::Collectors::Bind.new, + ) else - BindCollector.new + Arel::Collectors::SubstituteBinds.new( + self, + Arel::Collectors::SQLString.new, + ) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index c15b4a1a05..0afdd959f5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,13 +1,15 @@ -require_relative "abstract_adapter" -require_relative "statement_pool" -require_relative "mysql/column" -require_relative "mysql/explain_pretty_printer" -require_relative "mysql/quoting" -require_relative "mysql/schema_creation" -require_relative "mysql/schema_definitions" -require_relative "mysql/schema_dumper" -require_relative "mysql/schema_statements" -require_relative "mysql/type_metadata" +# frozen_string_literal: true + +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" +require "active_record/connection_adapters/mysql/column" +require "active_record/connection_adapters/mysql/explain_pretty_printer" +require "active_record/connection_adapters/mysql/quoting" +require "active_record/connection_adapters/mysql/schema_creation" +require "active_record/connection_adapters/mysql/schema_definitions" +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" @@ -15,13 +17,8 @@ module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting - include MySQL::ColumnDumper include MySQL::SchemaStatements - def update_table_definition(table_name, base) # :nodoc: - MySQL::Table.new(table_name, base) - end - ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -36,7 +33,7 @@ module ActiveRecord string: { name: "varchar", limit: 255 }, text: { name: "text", limit: 65535 }, integer: { name: "int", limit: 4 }, - float: { name: "float" }, + float: { name: "float", limit: 24 }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, timestamp: { name: "timestamp" }, @@ -47,7 +44,7 @@ module ActiveRecord json: { name: "json" }, } - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) stmt[:stmt].close end @@ -59,12 +56,12 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) if version < "5.1.10" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10." end end def version #:nodoc: - @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + @version ||= Version.new(version_string) end def mariadb? # :nodoc: @@ -120,11 +117,11 @@ module ActiveRecord end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 + query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1 end def native_database_types @@ -287,7 +284,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) - query_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<-SQL.strip_heredoc, "SCHEMA").presence SELECT table_comment FROM information_schema.tables WHERE table_schema = #{scope[:schema]} @@ -295,14 +292,10 @@ module ActiveRecord SQL end - def create_table(table_name, **options) #:nodoc: - super(table_name, options: "ENGINE=InnoDB", **options) - end - def bulk_change_table(table_name, operations) #:nodoc: sqls = operations.flat_map do |command, args| table, arguments = args.shift, args - method = :"#{command}_sql" + method = :"#{command}_for_alter" if respond_to?(method, true) send(method, table, *arguments) @@ -314,6 +307,11 @@ module ActiveRecord execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") end + def change_table_comment(table_name, comment) #:nodoc: + comment = "" if comment.nil? + execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}") + end + # Renames a table. # # Example: @@ -354,26 +352,27 @@ module ActiveRecord def change_column_default(table_name, column_name, default_or_changes) #:nodoc: default = extract_new_default_value(default_or_changes) - column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, default: default + change_column table_name, column_name, nil, default: default end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: - column = column_for(table_name, column_name) - unless null || default.nil? execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end - change_column table_name, column_name, column.sql_type, null: null + change_column table_name, column_name, nil, null: null + end + + def change_column_comment(table_name, column_name, comment) #:nodoc: + change_column table_name, column_name, nil, comment: comment end def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}") rename_column_indexes(table_name, column_name, new_column_name) end @@ -400,12 +399,13 @@ module ActiveRecord fk.constraint_name AS 'name', rc.update_rule AS 'on_update', rc.delete_rule AS 'on_delete' - FROM information_schema.key_column_usage fk - JOIN information_schema.referential_constraints rc + FROM information_schema.referential_constraints rc + JOIN information_schema.key_column_usage fk USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{scope[:schema]} AND fk.table_name = #{scope[:name]} + AND rc.constraint_schema = #{scope[:schema]} AND rc.table_name = #{scope[:name]} SQL @@ -545,7 +545,7 @@ module ActiveRecord execute("SET @@SESSION.sql_mode = #{sql_mode}") end - def initialize_type_map(m) + def initialize_type_map(m = type_map) super register_class_with_limit m, %r(char)i, MysqlString @@ -560,7 +560,6 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) - m.register_type %r(^json)i, Type::Json.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -603,25 +602,6 @@ module ActiveRecord end end - def add_index_length(quoted_columns, **options) - if length = options[:length] - case length - when Hash - length = length.symbolize_keys - quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } - when Integer - quoted_columns.each { |name, column| column << "(#{length})" } - end - end - - quoted_columns - end - - def add_options_for_index_columns(quoted_columns, **options) - quoted_columns = add_index_length(quoted_columns, options) - super - end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 @@ -632,6 +612,9 @@ module ActiveRecord ER_LOCK_DEADLOCK = 1213 ER_CANNOT_ADD_FOREIGN = 1215 ER_CANNOT_CREATE_TABLE = 1005 + ER_LOCK_WAIT_TIMEOUT = 1205 + ER_QUERY_INTERRUPTED = 1317 + ER_QUERY_TIMEOUT = 3024 def translate_exception(exception, message) case error_number(exception) @@ -655,19 +638,20 @@ module ActiveRecord NotNullViolation.new(message) when ER_LOCK_DEADLOCK Deadlocked.new(message) + when ER_LOCK_WAIT_TIMEOUT + LockWaitTimeout.new(message) + when ER_QUERY_TIMEOUT + StatementTimeout.new(message) + when ER_QUERY_INTERRUPTED + QueryCanceled.new(message) else super end end - def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition(table_name) - cd = td.new_column_definition(column_name, type, options) - schema_creation.accept(AddColumnDefinition.new(cd)) - end - - def change_column_sql(table_name, column_name, type, options = {}) + def change_column_for_alter(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) + type ||= column.sql_type unless options.key?(:default) options[:default] = column.default @@ -686,7 +670,7 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def rename_column_sql(table_name, column_name, new_column_name) + def rename_column_for_alter(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { default: column.default, @@ -700,31 +684,23 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end - - def remove_columns_sql(table_name, *column_names) - column_names.map { |column_name| remove_column_sql(table_name, column_name) } - end - - def add_index_sql(table_name, column_name, options = {}) + def add_index_for_alter(table_name, column_name, options = {}) index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) index_algorithm[0, 0] = ", " if index_algorithm.present? "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end - def remove_index_sql(table_name, options = {}) + def remove_index_for_alter(table_name, options = {}) index_name = index_name_for_remove(table_name, options) - "DROP INDEX #{index_name}" + "DROP INDEX #{quote_column_name(index_name)}" end - def add_timestamps_sql(table_name, options = {}) - [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -737,7 +713,8 @@ module ActiveRecord # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' subselect.distinct unless select.limit || select.offset || select.orders.any? - Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) + key_name = quote_column_name(key.name) + Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name)) end def supports_rename_index? @@ -758,7 +735,7 @@ module ActiveRecord defaults = [":default", :default].to_set # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables + # https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. if sql_mode = variables.delete("sql_mode") sql_mode = quote(sql_mode) @@ -775,7 +752,7 @@ module ActiveRecord sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 + # https://dev.mysql.com/doc/refman/5.7/en/set-names.html # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}".dup @@ -854,11 +831,15 @@ module ActiveRecord end end + def version_string + full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] + end + class MysqlString < Type::String # :nodoc: def serialize(value) case value - when true then MySQL::Quoting::QUOTED_TRUE - when false then MySQL::Quoting::QUOTED_FALSE + when true then "1" + when false then "0" else super end end @@ -867,8 +848,8 @@ module ActiveRecord def cast_value(value) case value - when true then MySQL::Quoting::QUOTED_TRUE - when false then MySQL::Quoting::QUOTED_FALSE + when true then "1" + when false then "0" else super end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 61cd7ae4cc..5d81de9fe1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # :stopdoc: module ConnectionAdapters @@ -9,11 +11,11 @@ module ActiveRecord # Instantiates a new column in the table. # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>. + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id bigint</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil) + def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **) @name = name.freeze @table_name = table_name @sql_type_metadata = sql_type_metadata diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 3e4ea28f63..508132accb 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "uri" module ActiveRecord @@ -181,13 +183,25 @@ module ActiveRecord raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + # Require the adapter itself and give useful feedback about + # 1. Missing adapter gems and + # 2. Adapter gems' missing dependencies. path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter" begin require path_to_adapter - rescue Gem::LoadError => e - raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + # We couldn't require the adapter itself. Raise an exception that + # points out config typos and missing gems. + if e.path == path_to_adapter + # We can assume that a non-builtin adapter was specified, so it's + # either misspelled or missing from Gemfile. + raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace + + # Bubbled up from the adapter require. Prefix the exception message + # with some guidance about how to address it and reraise. + else + raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace + end end adapter_method = "#{spec[:adapter]}_connection" diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 0fdc185c45..3dcb916d99 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module DetermineIfPreparableVisitor diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index c9ad47c035..fa1541019d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL 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 bda482a00f..a058a72872 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL module DatabaseStatements # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = [], preparable: nil) # :nodoc: + def select_all(*) # :nodoc: result = if ExplainRegistry.collect? && prepared_statements unprepared_statement { super } else diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb index 9691060cd3..20c3c83664 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index d4f5906b33..be038403b8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL module Quoting # :nodoc: - QUOTED_TRUE, QUOTED_FALSE = "1".freeze, "0".freeze - def quote_column_name(name) @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze end @@ -12,18 +12,10 @@ module ActiveRecord @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze end - def quoted_true - QUOTED_TRUE - end - def unquoted_true 1 end - def quoted_false - QUOTED_FALSE - end - def unquoted_false 0 end @@ -39,6 +31,13 @@ module ActiveRecord def quoted_binary(value) "x'#{value.hex}'" end + + def _type_cast(value) + case value + when Date, Time then value + else super + end + end end end 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 eea4984680..75377693c6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL @@ -28,7 +30,7 @@ module ActiveRecord # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP # column to contain NULL, explicitly declare it with the NULL attribute. - # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html + # See https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] sql << " NULL" unless options[:null] == false || options_include_default?(options) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 6d88c14d50..2ed4ad16ae 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -1,12 +1,9 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL module ColumnMethods - def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) - super - end - def blob(*args, **options) args.each { |name| column(name, :blob, options) } end @@ -35,10 +32,6 @@ module ActiveRecord args.each { |name| column(name, :longtext, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def unsigned_integer(*args, **options) args.each { |name| column(name, :unsigned_integer, options) } end @@ -66,7 +59,6 @@ module ActiveRecord when :primary_key type = :integer options[:limit] ||= 8 - options[:auto_increment] = true options[:primary_key] = true when /\Aunsigned_(?<type>.+)\z/ type = $~[:type].to_sym @@ -80,6 +72,11 @@ module ActiveRecord def aliased_types(name, fallback) fallback end + + def integer_like_primary_key_type(type, options) + options[:auto_increment] = true + type + end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index eff96e329f..d23178e43c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -1,25 +1,29 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL - module ColumnDumper # :nodoc: - def prepare_column_options(column) - spec = super - spec[:unsigned] = "true" if column.unsigned? - - if supports_virtual_columns? && column.virtual? - spec[:as] = extract_expression_for_virtual_column(column) - spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) - spec = { type: schema_type(column).inspect }.merge!(spec) - end + class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc: + private + def prepare_column_options(column) + spec = super + spec[:unsigned] = "true" if column.unsigned? + spec[:auto_increment] = "true" if column.auto_increment? - spec - end + if @connection.supports_virtual_columns? && column.virtual? + spec[:as] = extract_expression_for_virtual_column(column) + spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) + spec = { type: schema_type(column).inspect }.merge!(spec) + end - def migration_keys - super + [:unsigned] - end + spec + end - private + def column_spec_for_primary_key(column) + spec = super + spec.delete(:auto_increment) if column.type == :integer && column.auto_increment? + spec + end def default_primary_key?(column) super && column.auto_increment? && !column.unsigned? @@ -47,24 +51,27 @@ module ActiveRecord def schema_collation(column) if column.collation && table_name = column.table_name @table_collation_cache ||= {} - @table_collation_cache[table_name] ||= exec_query("SHOW TABLE STATUS LIKE #{quote(table_name)}", "SCHEMA").first["Collation"] + @table_collation_cache[table_name] ||= + @connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"] column.collation.inspect if column.collation != @table_collation_cache[table_name] end end def extract_expression_for_virtual_column(column) - if mariadb? - create_table_info = create_table_info(column.table_name) - if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info + if @connection.mariadb? && @connection.version < "10.2.5" + create_table_info = @connection.send(:create_table_info, column.table_name) + column_name = @connection.quote_column_name(column.name) + if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info $~[:expression].inspect end else - scope = quoted_scope(column.table_name) + scope = @connection.send(:quoted_scope, column.table_name) + column_name = @connection.quote(column.name) sql = "SELECT generation_expression FROM information_schema.columns" \ " WHERE table_schema = #{scope[:schema]}" \ " AND table_name = #{scope[:name]}" \ - " AND column_name = #{quote(column.name)}" - query_value(sql, "SCHEMA").inspect + " AND column_name = #{column_name}" + @connection.query_value(sql, "SCHEMA").inspect end end end 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 a01fbba201..ce50590651 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -1,15 +1,11 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL module SchemaStatements # :nodoc: # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - + def indexes(table_name) indexes = [] current_index = nil execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| @@ -26,23 +22,26 @@ module ActiveRecord index_using = mysql_index_type end - indexes << IndexDefinition.new( + indexes << [ row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, + [], + lengths: {}, + orders: {}, type: index_type, using: index_using, comment: row[:Index_comment].presence - ) + ] end - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + 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][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" end end - indexes + indexes.map { |index| IndexDefinition.new(*index) } end def remove_column(table_name, column_name, type = nil, options = {}) @@ -60,6 +59,14 @@ module ActiveRecord end end + def update_table_definition(table_name, base) + MySQL::Table.new(table_name, base) + end + + def create_schema_dumper(options) + MySQL::SchemaDumper.create(self, options) + end + private CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] @@ -73,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 && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) + default, default_function = nil, "CURRENT_TIMESTAMP" else default, default_function = field[:Default], nil end @@ -99,6 +106,18 @@ module ActiveRecord super unless specifier == "RESTRICT" end + def add_index_length(quoted_columns, **options) + lengths = options_for_index_columns(options[:length]) + quoted_columns.each do |name, column| + column << "(#{lengths[name]})" if lengths[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb index 9ad6a6c0d0..7ad0944d51 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module MySQL diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index c5c0a071e7..bfdc7995f0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,9 +1,10 @@ -require_relative "abstract_mysql_adapter" -require_relative "mysql/database_statements" +# frozen_string_literal: true -gem "mysql2", ">= 0.3.18", "< 0.5" +require "active_record/connection_adapters/abstract_mysql_adapter" +require "active_record/connection_adapters/mysql/database_statements" + +gem "mysql2", "~> 0.4.4" require "mysql2" -raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3" module ActiveRecord module ConnectionHandling # :nodoc: @@ -103,6 +104,11 @@ module ActiveRecord @connection.close end + def discard! # :nodoc: + @connection.automatic_close = false + @connection = nil + end + private def connect diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 3ad1911a28..469ef3f5a0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. @@ -5,11 +7,38 @@ module ActiveRecord delegate :array, :oid, :fmod, to: :sql_type_metadata alias :array? :array + def initialize(*, max_identifier_length: 63, **) + super + @max_identifier_length = max_identifier_length + end + def serial? return unless default_function - %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function + if %r{\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z} =~ default_function + sequence_name_from_parts(table_name, name, suffix) == sequence_name + end end + + protected + 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 + + if over_length > 0 + column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min + over_length -= column_name.length - column_name_length + column_name = column_name[0, column_name_length - [over_length, 0].min] + end + + if over_length > 0 + table_name = table_name[0, table_name.length - over_length] + end + + "#{table_name}_#{column_name}_#{suffix}" + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index ebf1715ed0..8db2a645af 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb index 99f3a5bbdf..086a5dcc15 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 6666622c08..542ca75d3e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,27 @@ -require_relative "oid/array" -require_relative "oid/bit" -require_relative "oid/bit_varying" -require_relative "oid/bytea" -require_relative "oid/cidr" -require_relative "oid/date_time" -require_relative "oid/decimal" -require_relative "oid/enum" -require_relative "oid/hstore" -require_relative "oid/inet" -require_relative "oid/jsonb" -require_relative "oid/money" -require_relative "oid/oid" -require_relative "oid/point" -require_relative "oid/legacy_point" -require_relative "oid/range" -require_relative "oid/specialized_string" -require_relative "oid/uuid" -require_relative "oid/vector" -require_relative "oid/xml" +# frozen_string_literal: true -require_relative "oid/type_map_initializer" +require "active_record/connection_adapters/postgresql/oid/array" +require "active_record/connection_adapters/postgresql/oid/bit" +require "active_record/connection_adapters/postgresql/oid/bit_varying" +require "active_record/connection_adapters/postgresql/oid/bytea" +require "active_record/connection_adapters/postgresql/oid/cidr" +require "active_record/connection_adapters/postgresql/oid/date_time" +require "active_record/connection_adapters/postgresql/oid/decimal" +require "active_record/connection_adapters/postgresql/oid/enum" +require "active_record/connection_adapters/postgresql/oid/hstore" +require "active_record/connection_adapters/postgresql/oid/inet" +require "active_record/connection_adapters/postgresql/oid/jsonb" +require "active_record/connection_adapters/postgresql/oid/money" +require "active_record/connection_adapters/postgresql/oid/oid" +require "active_record/connection_adapters/postgresql/oid/point" +require "active_record/connection_adapters/postgresql/oid/legacy_point" +require "active_record/connection_adapters/postgresql/oid/range" +require "active_record/connection_adapters/postgresql/oid/specialized_string" +require "active_record/connection_adapters/postgresql/oid/uuid" +require "active_record/connection_adapters/postgresql/oid/vector" +require "active_record/connection_adapters/postgresql/oid/xml" + +require "active_record/connection_adapters/postgresql/oid/type_map_initializer" module ActiveRecord module ConnectionAdapters 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 a73a8c1726..d6852082ac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL 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 0a505f46a7..587e95d192 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb index 4c21097d48..dc7079dda2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 702fa8175c..a3c60ecef6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 5225609e37..66e99d9404 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "ipaddr" module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index b7acbf7178..cd667422f5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index 43d22c8daf..e7d33855c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + BigDecimal("Infinity") * (options[:negative] ? -1 : 1) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 950d23d516..f70f09ad95 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 49dd4fc73f..aabe83b85d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb index 96486fa65b..55be71fd26 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index a1fec289d4..e0216f1089 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb index 775eecaf85..7b057a8452 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 7a91272d1c..6434377b57 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -22,7 +24,7 @@ module ActiveRecord # (3) -$2.55 # (4) ($2.55) - value.sub!(/^\((.+)\)$/, '-\1') # (4) + value = value.sub(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, "") diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb index 9c2ac08b30..d8c044320d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 7c764e7287..02a9c506f6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord Point = Struct.new(:x, :y) 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 54d5d0902e..6edb7cfd3c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -33,7 +35,7 @@ module ActiveRecord if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) - "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + ::Range.new(from, to, value.exclude_end?) else super end @@ -58,7 +60,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? "" : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(value) end def extract_bounds(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 564e82a4ac..4ad1344f05 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index d9ae1aa7a2..231278c184 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -29,8 +31,8 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end - def query_conditions_for_initial_load(type_map) - known_type_names = type_map.keys.map { |n| "'#{n}'" } + def query_conditions_for_initial_load + known_type_names = @store.keys.map { |n| "'#{n}'" } known_type_types = %w('r' 'e' 'd') <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] WHERE diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 5e839228e9..bc9b8dbfcf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: - ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x + ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z} alias_method :serialize, :deserialize diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index b26e876b54..88ef626a16 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb index d40d837cee..042f32fdc3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index ee4230c6f2..e75202b0be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -62,7 +64,7 @@ module ActiveRecord def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && /\(\)/.match?(value) + elsif column.type == :uuid && value.is_a?(String) && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) @@ -99,6 +101,8 @@ module ActiveRecord end when OID::Array::Data _quote(encode_array(value)) + when Range + _quote(encode_range(value)) else super end @@ -115,6 +119,8 @@ module ActiveRecord value.to_s when OID::Array::Data encode_array(value) + when Range + encode_range(value) else super end @@ -131,6 +137,10 @@ module ActiveRecord result end + def encode_range(range) + "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" + end + def determine_encoding_of_strings_in_array(value) case value when ::Array then determine_encoding_of_strings_in_array(value.first) @@ -144,6 +154,14 @@ module ActiveRecord else _type_cast(values) end end + + def type_cast_range_value(value) + infinity?(value) ? "" : type_cast(value) + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index 44a7338bf5..8df91c988b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -1,27 +1,24 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL module ReferentialIntegrity # :nodoc: - def supports_disable_referential_integrity? # :nodoc: - true - end - def disable_referential_integrity # :nodoc: - if supports_disable_referential_integrity? - original_exception = nil + original_exception = nil - begin - transaction(requires_new: true) do - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - end - rescue ActiveRecord::ActiveRecordError => e - original_exception = e + begin + transaction(requires_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) end + rescue ActiveRecord::ActiveRecordError => e + original_exception = e + end - begin - yield - rescue ActiveRecord::InvalidForeignKey => e - warn <<-WARNING + begin + yield + rescue ActiveRecord::InvalidForeignKey => e + warn <<-WARNING WARNING: Rails was not able to disable referential integrity. This is most likely caused due to missing permissions. @@ -30,17 +27,14 @@ Rails needs superuser privileges to disable referential integrity. cause: #{original_exception.try(:message)} WARNING - raise e - end + raise e + end - begin - transaction(requires_new: true) do - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - rescue ActiveRecord::ActiveRecordError + begin + transaction(requires_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) end - else - yield + rescue ActiveRecord::ActiveRecordError end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index e1d5089115..8e381a92cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -1,8 +1,22 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private + def visit_AlterTable(o) + super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") + end + + def visit_AddForeignKey(o) + super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? } + end + + def visit_ValidateConstraint(name) + "VALIDATE CONSTRAINT #{quote_column_name(name)}" + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" 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 11ea1e5144..6047217fcd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -42,15 +44,8 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) if type == :uuid options[:default] = options.fetch(:default, "gen_random_uuid()") - elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type) - type = if type == :bigint || options[:limit] == 8 - :bigserial - else - :serial - end end super @@ -100,10 +95,6 @@ module ActiveRecord args.each { |name| column(name, :int8range, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def jsonb(*args, **options) args.each { |name| column(name, :jsonb, options) } end @@ -183,11 +174,33 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + + private + def integer_like_primary_key_type(type, options) + if type == :bigint || options[:limit] == 8 + :bigserial + else + :serial + end + end end class Table < ActiveRecord::ConnectionAdapters::Table include ColumnMethods end + + class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable + attr_reader :constraint_validations + + def initialize(td) + super + @constraint_validations = [] + end + + def validate_constraint(name) + @constraint_validations << name + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index 5555a46b6b..84643d20da 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -1,21 +1,28 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL - module ColumnDumper # :nodoc: - # Adds +:array+ option to the default set - def prepare_column_options(column) - spec = super - spec[:array] = "true" if column.array? - spec - end - - # Adds +:array+ as a valid migration key - def migration_keys - super + [:array] - end - + class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc: private + def extensions(stream) + extensions = @connection.extensions + if extensions.any? + stream.puts " # These are extensions that must be enabled in order to support this database" + extensions.sort.each do |extension| + stream.puts " enable_extension #{extension.inspect}" + end + stream.puts + end + end + + def prepare_column_options(column) + spec = super + spec[:array] = "true" if column.array? + spec + end + def default_primary_key?(column) schema_type(column) == :bigserial end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index a710ea6cc9..a8895f8606 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/string/strip" +# frozen_string_literal: true module ActiveRecord module ConnectionAdapters @@ -64,12 +64,7 @@ module ActiveRecord end # Verifies existence of an index with a given name. - def index_name_exists?(table_name, index_name, default = nil) - unless default.nil? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing default to #index_name_exists? is deprecated without replacement. - MSG - end + def index_name_exists?(table_name, index_name) table = quoted_scope(table_name) index = quoted_scope(index_name) @@ -87,21 +82,12 @@ module ActiveRecord end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) # :nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - + def indexes(table_name) # :nodoc: scope = quoted_scope(table_name) result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, - pg_catalog.obj_description(i.oid, 'pg_class') AS comment, - (SELECT COUNT(*) FROM pg_opclass o - JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c - ON o.oid = c.oid WHERE o.opcdefault = 'f') + pg_catalog.obj_description(i.oid, 'pg_class') AS comment FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid @@ -120,11 +106,13 @@ module ActiveRecord inddef = row[3] oid = row[4] comment = row[5] - opclass = row[6] using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - if indkey.include?(0) || opclass > 0 + orders = {} + opclasses = {} + + if indkey.include?(0) columns = expressions else columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact @@ -134,10 +122,12 @@ module ActiveRecord AND a.attnum IN (#{indkey.join(",")}) SQL - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - orders = Hash[ - expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } - ] + # 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| + opclasses[column] = opclass.to_sym if opclass + orders[column] = :desc if desc + end end IndexDefinition.new( @@ -146,6 +136,7 @@ module ActiveRecord unique, columns, orders: orders, + opclasses: opclasses, where: where, using: using.to_sym, comment: comment.presence @@ -222,7 +213,7 @@ module ActiveRecord # Sets the schema search path to a string of comma-separated schema names. # Names beginning with $ have to be quoted (e.g. $user => '$user'). - # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html + # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html # # This should be not be called manually but set in database.yml. def schema_search_path=(schema_csv) @@ -373,6 +364,31 @@ module ActiveRecord SQL end + def bulk_change_table(table_name, operations) + sql_fragments = [] + non_combinable_operations = [] + + operations.each do |command, args| + table, arguments = args.shift, args + method = :"#{command}_for_alter" + + 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? + else + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + sql_fragments = [] + non_combinable_operations = [] + send(command, table, *arguments) + end + end + + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + end + # Renames a table. # Also renames a table's primary key sequence if the sequence name exists and # matches the Active Record default. @@ -403,50 +419,23 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! - quoted_table_name = quote_table_name(table_name) - quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options) - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - if options[:using] - sql << " USING #{options[:using]}" - elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options) - sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" - end - execute sql - - change_column_default(table_name, column_name, options[:default]) if options.key?(:default) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) } + execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}" + procs.each(&:call) end # Changes the default value of a table column. def change_column_default(table_name, column_name, default_or_changes) # :nodoc: - clear_cache! - column = column_for(table_name, column_name) - return unless column - - default = extract_new_default_value(default_or_changes) - alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" - if default.nil? - # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will - # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". - execute alter_column_query % "DROP DEFAULT" - else - execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" - end + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}" end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}" end # Adds comment for given table column or drops it if +comment+ is a +nil+ @@ -469,8 +458,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment end end @@ -510,7 +499,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) fk_info = exec_query(<<-SQL.strip_heredoc, "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 + 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 JOIN pg_class t2 ON c.confrelid = t2.oid @@ -532,6 +521,7 @@ module ActiveRecord options[:on_delete] = extract_foreign_key_action(row["on_delete"]) options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:validate] = row["valid"] ForeignKeyDefinition.new(table_name, row["to_table"], options) end @@ -584,6 +574,51 @@ module ActiveRecord [super, *order_columns].join(", ") end + def update_table_definition(table_name, base) # :nodoc: + PostgreSQL::Table.new(table_name, base) + end + + def create_schema_dumper(options) # :nodoc: + PostgreSQL::SchemaDumper.create(self, options) + end + + # Validates the given constraint. + # + # Validates the constraint named +constraint_name+ on +accounts+. + # + # validate_constraint :accounts, :constraint_name + def validate_constraint(table_name, constraint_name) + return unless supports_validate_constraints? + + at = create_alter_table table_name + at.validate_constraint constraint_name + + execute schema_creation.accept(at) + end + + # Validates the given foreign key. + # + # Validates the foreign key on +accounts.branch_id+. + # + # validate_foreign_key :accounts, :branches + # + # Validates the foreign key on +accounts.owner_id+. + # + # validate_foreign_key :accounts, column: :owner_id + # + # Validates the foreign key named +special_fk_name+ on the +accounts+ table. + # + # validate_foreign_key :accounts, name: :special_fk_name + # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + def validate_foreign_key(from_table, options_or_to_table = {}) + return unless supports_validate_constraints? + + fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name + + validate_constraint from_table, fk_name_to_validate + end + private def schema_creation PostgreSQL::SchemaCreation.new(self) @@ -593,6 +628,10 @@ module ActiveRecord PostgreSQL::TableDefinition.new(*args) end + def create_alter_table(name) + PostgreSQL::AlterTable.new create_table_definition(name) + end + def new_column_from_field(table_name, field) column_name, type, default, notnull, oid, fmod, collation, comment = field type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) @@ -607,7 +646,8 @@ module ActiveRecord table_name, default_function, collation, - comment: comment.presence + comment: comment.presence, + max_identifier_length: max_identifier_length ) end @@ -631,6 +671,72 @@ module ActiveRecord end end + def change_column_sql(table_name, column_name, type, options = {}) + quoted_column_name = quote_column_name(column_name) + sql_type = type_to_sql(type, options) + sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + sql + end + + def change_column_for_alter(table_name, column_name, type, options = {}) + sqls = [change_column_sql(table_name, column_name, type, options)] + sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) + sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment) + sqls + end + + + # Changes the default value of a table column. + def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: + column = column_for(table_name, column_name) + return unless column + + default = extract_new_default_value(default_or_changes) + alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + alter_column_query % "DROP DEFAULT" + else + alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" + end + end + + def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: + "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" + end + + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] + end + + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] + end + + def add_index_opclass(quoted_columns, **options) + opclasses = options_for_index_columns(options[:opclass]) + quoted_columns.each do |name, column| + column << " #{opclasses[name]}" if opclasses[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_opclass(quoted_columns, options) + super + end + 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 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 f57179ae59..b252a76caa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index aa7940188a..bfd300723d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 8baef19030..441be47fa1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility gem "pg", "~> 0.18" require "pg" -require_relative "abstract_adapter" -require_relative "statement_pool" -require_relative "postgresql/column" -require_relative "postgresql/database_statements" -require_relative "postgresql/explain_pretty_printer" -require_relative "postgresql/oid" -require_relative "postgresql/quoting" -require_relative "postgresql/referential_integrity" -require_relative "postgresql/schema_creation" -require_relative "postgresql/schema_definitions" -require_relative "postgresql/schema_dumper" -require_relative "postgresql/schema_statements" -require_relative "postgresql/type_metadata" -require_relative "postgresql/utils" +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" +require "active_record/connection_adapters/postgresql/column" +require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/explain_pretty_printer" +require "active_record/connection_adapters/postgresql/oid" +require "active_record/connection_adapters/postgresql/quoting" +require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_creation" +require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_dumper" +require "active_record/connection_adapters/postgresql/schema_statements" +require "active_record/connection_adapters/postgresql/type_metadata" +require "active_record/connection_adapters/postgresql/utils" module ActiveRecord module ConnectionHandling # :nodoc: @@ -62,11 +64,11 @@ module ActiveRecord # defaults to true. # # Any further options are used as connection parameters to libpq. See - # http://www.postgresql.org/docs/current/static/libpq-connect.html for the + # https://www.postgresql.org/docs/current/static/libpq-connect.html for the # list of parameters. # # In addition, default connection parameters of libpq can be set per environment variables. - # See http://www.postgresql.org/docs/current/static/libpq-envars.html . + # See https://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = "PostgreSQL".freeze @@ -74,7 +76,7 @@ module ActiveRecord primary_key: "bigserial primary key", string: { name: "character varying" }, text: { name: "text" }, - integer: { name: "integer" }, + integer: { name: "integer", limit: 4 }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, @@ -119,7 +121,10 @@ module ActiveRecord include PostgreSQL::ReferentialIntegrity include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements - include PostgreSQL::ColumnDumper + + def supports_bulk_alter? + true + end def supports_index_sort_order? true @@ -141,6 +146,10 @@ module ActiveRecord true end + def supports_validate_constraints? + true + end + def supports_views? true end @@ -165,7 +174,7 @@ module ActiveRecord { concurrently: "CONCURRENTLY" } end - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: def initialize(connection, max) super(max) @connection = connection @@ -181,9 +190,9 @@ module ActiveRecord end private - def dealloc(key) @connection.query "DEALLOCATE #{key}" if connection_active? + rescue PG::Error end def connection_active? @@ -215,7 +224,7 @@ module ActiveRecord add_pg_decoders @type_map = Type::HashLookupTypeMap.new - initialize_type_map(type_map) + initialize_type_map @local_tz = execute("SHOW TIME ZONE", "SCHEMA").first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end @@ -271,6 +280,11 @@ module ActiveRecord end end + def discard! # :nodoc: + @connection.socket_io.reopen(IO::NULL) + @connection = nil + end + def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end @@ -310,14 +324,14 @@ module ActiveRecord def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer") end query_value("SELECT pg_try_advisory_lock(#{lock_id})") end def release_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer") end query_value("SELECT pg_advisory_unlock(#{lock_id})") end @@ -335,25 +349,20 @@ module ActiveRecord end def extension_enabled?(name) - if supports_extensions? - res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA") - res.cast_values.first - end + res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA") + res.cast_values.first end def extensions - if supports_extensions? - exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values - else - super - end + exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values end # Returns the configured supported identifier length supported by PostgreSQL - def table_alias_length + def max_identifier_length @max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i end - alias index_name_length table_alias_length + alias table_alias_length max_identifier_length + alias index_name_length max_identifier_length # Set the authorized user for this session def session_auth=(user) @@ -365,10 +374,6 @@ module ActiveRecord @use_insert_returning end - def update_table_definition(table_name, base) #:nodoc: - PostgreSQL::Table.new(table_name, base) - end - def column_name_for_operation(operation, node) # :nodoc: OPERATION_ALIASES.fetch(operation) { operation.downcase } end @@ -389,8 +394,7 @@ module ActiveRecord end private - - # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html + # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" NOT_NULL_VIOLATION = "23502" @@ -398,6 +402,8 @@ module ActiveRecord UNIQUE_VIOLATION = "23505" SERIALIZATION_FAILURE = "40001" DEADLOCK_DETECTED = "40P01" + LOCK_NOT_AVAILABLE = "55P03" + QUERY_CANCELED = "57014" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) @@ -417,6 +423,10 @@ module ActiveRecord SerializationFailure.new(message) when DEADLOCK_DETECTED Deadlocked.new(message) + when LOCK_NOT_AVAILABLE + LockWaitTimeout.new(message) + when QUERY_CANCELED + QueryCanceled.new(message) else super end @@ -424,7 +434,7 @@ module ActiveRecord def get_oid_type(oid, fmod, column_name, sql_type = "".freeze) if !type_map.key?(oid) - load_additional_types(type_map, [oid]) + load_additional_types([oid]) end type_map.fetch(oid, fmod, sql_type) { @@ -435,10 +445,10 @@ module ActiveRecord } end - def initialize_type_map(m) - register_class_with_limit m, "int2", Type::Integer - register_class_with_limit m, "int4", Type::Integer - register_class_with_limit m, "int8", Type::Integer + def initialize_type_map(m = type_map) + m.register_type "int2", Type::Integer.new(limit: 2) + m.register_type "int4", Type::Integer.new(limit: 4) + m.register_type "int8", Type::Integer.new(limit: 8) m.register_type "oid", OID::Oid.new m.register_type "float4", Type::Float.new m.alias_type "float8", "float4" @@ -502,18 +512,7 @@ module ActiveRecord end end - load_additional_types(m) - end - - def extract_limit(sql_type) - case sql_type - when /^bigint/i, /^int8/i - 8 - when /^smallint/i - 2 - else - super - end + load_additional_types end # Extracts the value from a PostgreSQL column default definition. @@ -551,7 +550,7 @@ module ActiveRecord !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default) end - def load_additional_types(type_map, oids = nil) + def load_additional_types(oids = nil) initializer = OID::TypeMapInitializer.new(type_map) if supports_ranges? @@ -570,7 +569,7 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") else - query += initializer.query_conditions_for_initial_load(type_map) + query += initializer.query_conditions_for_initial_load end execute_and_clear(query, "SCHEMA", []) do |records| @@ -635,7 +634,7 @@ module ActiveRecord # ActiveRecord::PreparedStatementCacheExpired # # Check here for more details: - # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 + # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze def is_cached_plan_failure?(e) pgerror = e.cause @@ -700,18 +699,20 @@ module ActiveRecord # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings + variables = @config.fetch(:variables, {}).stringify_keys + # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. - # (SET TIME ZONE does not use an equals sign like other SET variables) - if ActiveRecord::Base.default_timezone == :utc - execute("SET time zone 'UTC'", "SCHEMA") - elsif @local_tz - execute("SET time zone '#{@local_tz}'", "SCHEMA") + unless variables["timezone"] + if ActiveRecord::Base.default_timezone == :utc + variables["timezone"] = "UTC" + elsif @local_tz + variables["timezone"] = @local_tz + end end # SET statements from :variables config hash - # http://www.postgresql.org/docs/current/static/sql-set.html - variables = @config[:variables] || {} + # https://www.postgresql.org/docs/current/static/sql-set.html variables.map do |k, v| if v == ":default" || v == :default # Sets the value to the global or compile default diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4d339b0a8c..f34b6733da 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters class SchemaCache diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb index 9e12ae0de8..8489bcbf1d 100644 --- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # :stopdoc: module ConnectionAdapters diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb index 6fe3e1211e..832fdfe5c4 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 7276a65098..8042dbfea2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 @@ -22,6 +24,22 @@ module ActiveRecord "x'#{value.hex}'" end + def quoted_true + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1".freeze : "'t'".freeze + end + + def unquoted_true + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t".freeze + end + + def quoted_false + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0".freeze : "'f'".freeze + end + + def unquoted_false + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f".freeze + end + private def _type_cast(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index bc798d1dbb..b842561317 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb index e157e4b218..c9855019c1 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -1,27 +1,18 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 - module ColumnMethods - def primary_key(name, type = :primary_key, **options) - if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default)) - type = :primary_key - end - - super - end - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - def references(*args, **options) super(*args, type: :integer, **options) end alias :belongs_to :references - end - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods + private + def integer_like_primary_key_type(type, options) + :primary_key + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb index eec018eda3..621678ec65 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 - module ColumnDumper # :nodoc: + class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc: private - def default_primary_key?(column) schema_type(column) == :integer end 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 31e83f9260..58e5138e02 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -1,15 +1,11 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters module SQLite3 module SchemaStatements # :nodoc: # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - + def indexes(table_name) exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| index_sql = query_value(<<-SQL, "SCHEMA") SELECT sql @@ -27,16 +23,30 @@ module ActiveRecord col["name"] end + # Add info on sort order for columns (only desc order is explicitly specified, asc is + # the default) + orders = {} + if index_sql # index_sql can be null in case of primary key indexes + index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column| + orders[order_column] = :desc + } + end + IndexDefinition.new( table_name, row["name"], row["unique"] != 0, columns, - where: where + where: where, + orders: orders ) end end + def create_schema_dumper(options) + SQLite3::SchemaDumper.create(self, options) + end + private def schema_creation SQLite3::SchemaCreation.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 04129841e4..d4f5bd16ac 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,11 +1,13 @@ -require_relative "abstract_adapter" -require_relative "statement_pool" -require_relative "sqlite3/explain_pretty_printer" -require_relative "sqlite3/quoting" -require_relative "sqlite3/schema_creation" -require_relative "sqlite3/schema_definitions" -require_relative "sqlite3/schema_dumper" -require_relative "sqlite3/schema_statements" +# frozen_string_literal: true + +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" +require "active_record/connection_adapters/sqlite3/explain_pretty_printer" +require "active_record/connection_adapters/sqlite3/quoting" +require "active_record/connection_adapters/sqlite3/schema_creation" +require "active_record/connection_adapters/sqlite3/schema_definitions" +require "active_record/connection_adapters/sqlite3/schema_dumper" +require "active_record/connection_adapters/sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -55,11 +57,10 @@ module ActiveRecord ADAPTER_NAME = "SQLite".freeze include SQLite3::Quoting - include SQLite3::ColumnDumper include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { - primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", + primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL", string: { name: "varchar" }, text: { name: "text" }, integer: { name: "integer" }, @@ -69,21 +70,34 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - boolean: { name: "boolean" } + boolean: { name: "boolean" }, + json: { name: "json" }, } - class StatementPool < ConnectionAdapters::StatementPool - private + ## + # :singleton-method: + # Indicates whether boolean values are stored in sqlite3 databases as 1 + # and 0 or 't' and 'f'. Leaving <tt>ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer</tt> + # set to false is deprecated. SQLite databases have used 't' and 'f' to + # serialize boolean values and must have old data converted to 1 and 0 + # (its native boolean serialization) before setting this flag to true. + # Conversion can be accomplished by setting up a rake task which runs + # + # ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) + # ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) + # for all models and all boolean columns, after which the flag must be set + # to true by adding the following to your <tt>application.rb</tt> file: + # + # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true + class_attribute :represent_boolean_as_integer, default: false + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: + private def dealloc(stmt) stmt[:stmt].close unless stmt[:stmt].closed? end end - def update_table_definition(table_name, base) # :nodoc: - SQLite3::Table.new(table_name, base) - end - def initialize(connection, logger, connection_options, config) super(connection, logger, config) @@ -121,6 +135,10 @@ module ActiveRecord true end + def supports_json? + true + end + def supports_multi_insert? sqlite_version >= "3.7.11" end @@ -272,19 +290,18 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # See: http://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_type?(type) - type.to_sym != :primary_key + def valid_alter_table_type?(type, options = {}) + !invalid_alter_table_type?(type, options) end + deprecate :valid_alter_table_type? def add_column(table_name, column_name, type, options = {}) #:nodoc: - if valid_alter_table_type?(type) - super(table_name, column_name, type, options) - else + if invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| definition.column(column_name, type, options) end + else + super end end @@ -356,6 +373,10 @@ module ActiveRecord end private + def initialize_type_map(m = type_map) + super + register_class_with_limit m, %r(int)i, SQLite3Integer + end def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") @@ -364,6 +385,12 @@ module ActiveRecord end alias column_definitions table_structure + # See: https://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def invalid_alter_table_type?(type, options) + type.to_sym == :primary_key || options[:primary_key] + end + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" caller = lambda { |definition| yield definition if block_given? } @@ -385,18 +412,21 @@ module ActiveRecord options[:id] = false create_table(to, options) do |definition| @definition = definition - @definition.primary_key(from_primary_key) if from_primary_key.present? + if from_primary_key.is_a?(Array) + @definition.primary_keys from_primary_key + end columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name - next if column_name == from_primary_key @definition.column(column_name, column.type, limit: column.limit, default: column.default, precision: column.precision, scale: column.scale, - null: column.null, collation: column.collation) + null: column.null, collation: column.collation, + primary_key: column_name == from_primary_key + ) end yield @definition if block_given? end @@ -409,6 +439,9 @@ 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}" @@ -424,6 +457,7 @@ module ActiveRecord # index name can't be the same opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true } opts[:unique] = true if index.unique + opts[:where] = index.where if index.where add_index(to, columns, opts) end end @@ -511,6 +545,18 @@ module ActiveRecord def configure_connection execute("PRAGMA foreign_keys = ON", "SCHEMA") end + + class SQLite3Integer < Type::Integer # :nodoc: + private + def _limit + # INTEGER storage class can be stored 8 bytes value. + # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes + limit || 8 + end + end + + ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3) end + ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter) end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 790db56185..46bd831da7 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters class StatementPool # :nodoc: diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index b8fbb489b6..88d28dc52a 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionHandling RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } @@ -138,6 +140,6 @@ module ActiveRecord end delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, to: :connection_handler + :clear_all_connections!, :flush_idle_connections!, to: :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 198c712abc..88810cb328 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,7 +1,8 @@ -require "thread" +# frozen_string_literal: true + require "active_support/core_ext/hash/indifferent_access" -require "active_support/core_ext/object/duplicable" require "active_support/core_ext/string/filters" +require "concurrent/map" module ActiveRecord module Core @@ -17,6 +18,13 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## + # :singleton-method: + # + # Specifies if the methods calling database queries should be logged below + # their relevant queries. Defaults to false. + mattr_accessor :verbose_query_logs, instance_writer: false, default: false + + ## # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -75,25 +83,13 @@ module ActiveRecord # scope being ignored is error-worthy, rather than a warning. mattr_accessor :error_on_ignored_order, instance_writer: false, default: false - def self.error_on_ignored_order_or_limit - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The flag error_on_ignored_order_or_limit is deprecated. Limits are - now supported. Please use error_on_ignored_order instead. - MSG - error_on_ignored_order - end - - def error_on_ignored_order_or_limit - self.class.error_on_ignored_order_or_limit - end - - def self.error_on_ignored_order_or_limit=(value) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The flag error_on_ignored_order_or_limit is deprecated. Limits are - now supported. Please use error_on_ignored_order= instead. - MSG - self.error_on_ignored_order = value - end + # :singleton-method: + # Specify the behavior for unsafe raw query methods. Values are as follows + # deprecated - Warnings are logged when unsafe raw SQL is passed to + # query methods. + # disabled - Unsafe raw SQL passed to query methods results in + # UnknownAttributeReference exception. + mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated ## # :singleton-method: @@ -142,14 +138,14 @@ module ActiveRecord self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new end - module ClassMethods + module ClassMethods # :nodoc: def allocate define_attribute_methods super end def initialize_find_by_cache # :nodoc: - @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) } + @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end def inherited(child_class) # :nodoc: @@ -168,8 +164,7 @@ module ActiveRecord id = ids.first - return super if id.kind_of?(Array) || - id.is_a?(ActiveRecord::Base) + return super if StatementCache.unsupported_value?(id) key = primary_key @@ -177,7 +172,7 @@ module ActiveRecord where(key => params.bind).limit(1) } - record = statement.execute([id], self, connection).first + record = statement.execute([id], connection).first unless record raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id) @@ -194,7 +189,7 @@ module ActiveRecord hash = args.first return super if !(Hash === hash) || hash.values.any? { |v| - v.nil? || Array === v || Hash === v || Relation === v || Base === v + StatementCache.unsupported_value?(v) } # We can't cache Post.find_by(author: david) ...yet @@ -209,7 +204,7 @@ module ActiveRecord where(wheres).limit(1) } begin - statement.execute(hash.values, self, connection).first + statement.execute(hash.values, connection).first rescue TypeError raise ActiveRecord::StatementInvalid rescue ::RangeError @@ -251,7 +246,7 @@ module ActiveRecord end end - # Overwrite the default class equality method to provide support for association proxies. + # Overwrite the default class equality method to provide support for decorated models. def ===(object) object.is_a?(self) end @@ -265,16 +260,6 @@ module ActiveRecord @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end - # Returns the Arel engine. - def arel_engine # :nodoc: - @arel_engine ||= - if Base == self || connection_handler.retrieve_connection_pool(connection_specification_name) - self - else - superclass.arel_engine - end - end - def arel_attribute(name, table = arel_table) # :nodoc: name = attribute_alias(name) if attribute_alias?(name) table[name] @@ -292,16 +277,15 @@ module ActiveRecord def cached_find_by_statement(key, &block) cache = @find_by_statement_cache[connection.prepared_statements] - cache[key] || cache.synchronize { - cache[key] ||= StatementCache.create(connection, &block) - } + cache.compute_if_absent(key) { StatementCache.create(connection, &block) } end def relation relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? && !ignore_default_scope? - relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) + relation.where!(type_condition) + relation.create_with!(inheritance_column.to_s => sti_name) else relation end @@ -541,7 +525,7 @@ module ActiveRecord # # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here. # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html + # See also https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html def to_ary nil end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index cbd71a3779..ee4f818cbf 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record Counter Cache module CounterCache @@ -51,7 +53,7 @@ module ActiveRecord unscoped.where(primary_key => object.id).update_all(updates) end - return true + true end # A generic "counter updater" implementation, intended primarily to be @@ -180,7 +182,6 @@ module ActiveRecord each_counter_cached_associations do |association| if send(association.reflection.name) association.increment_counters - @_after_create_counter_called = true end end diff --git a/activerecord/lib/active_record/define_callbacks.rb b/activerecord/lib/active_record/define_callbacks.rb index 7d955a24be..87ecd7cec5 100644 --- a/activerecord/lib/active_record/define_callbacks.rb +++ b/activerecord/lib/active_record/define_callbacks.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module ActiveRecord - # This module exists because `ActiveRecord::AttributeMethods::Dirty` needs to - # define callbacks, but continue to have its version of `save` be the super - # method of `ActiveRecord::Callbacks`. This will be removed when the removal + # This module exists because ActiveRecord::AttributeMethods::Dirty needs to + # define callbacks, but continue to have its version of +save+ be the super + # method of ActiveRecord::Callbacks. This will be removed when the removal # of deprecated code removes this need. module DefineCallbacks extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 3a9625092e..3bb8c6f4e3 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module DynamicMatchers #:nodoc: private diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 12ef58a941..1a3e6e4d09 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/object/deep_dup" module ActiveRecord @@ -219,6 +221,8 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) raise_conflict_error(enum_name, method_name, type: "class") + elsif klass_method && method_defined_within?(method_name, Relation) + raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name) elsif !klass_method && dangerous_attribute_method?(method_name) raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 60d4fb70e0..efcbd44776 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record Errors # @@ -167,7 +169,7 @@ module ActiveRecord class NoDatabaseError < StatementInvalid end - # Raised when Postgres returns 'cached plan must not change result type' and + # Raised when PostgreSQL returns 'cached plan must not change result type' and # we cannot retry gracefully (e.g. inside a transaction) class PreparedStatementCacheExpired < StatementInvalid end @@ -313,7 +315,7 @@ module ActiveRecord # # See the following: # - # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://www.postgresql.org/docs/current/static/transaction-iso.html # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock class TransactionRollbackError < StatementInvalid end @@ -332,4 +334,41 @@ module ActiveRecord # +reverse_order+ to automatically reverse. class IrreversibleOrderError < ActiveRecordError end + + # LockWaitTimeout will be raised when lock wait timeout exceeded. + class LockWaitTimeout < StatementInvalid + end + + # StatementTimeout will be raised when statement timeout exceeded. + class StatementTimeout < StatementInvalid + end + + # QueryCanceled will be raised when canceling statement due to user request. + class QueryCanceled < StatementInvalid + end + + # UnknownAttributeReference is raised when an unknown and potentially unsafe + # value is passed to a query method when allow_unsafe_raw_sql is set to + # :disabled. For example, passing a non column name value to a relation's + # #order method might cause this exception. + # + # When working around this exception, caution should be taken to avoid SQL + # injection vulnerabilities when passing user-provided values to query + # methods. Known-safe values can be passed to query methods by wrapping them + # in Arel.sql. + # + # For example, with allow_unsafe_raw_sql set to :disabled, the following + # code would raise this exception: + # + # Post.order("length(title)").first + # + # The desired result can be accomplished by wrapping the known-safe string + # in Arel.sql: + # + # Post.order(Arel.sql("length(title)")).first + # + # Again, such a workaround should *not* be used when passing user-provided + # values, such as request parameters or model attributes to query methods. + class UnknownAttributeReference < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index eff5990f3a..7ccb938888 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,4 +1,6 @@ -require_relative "explain_registry" +# frozen_string_literal: true + +require "active_record/explain_registry" module ActiveRecord module Explain diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb index ef1ce3dc85..7fd078941a 100644 --- a/activerecord/lib/active_record/explain_registry.rb +++ b/activerecord/lib/active_record/explain_registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/per_thread_registry" module ActiveRecord diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 928720d011..a86217abc0 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "active_support/notifications" -require_relative "explain_registry" +require "active_record/explain_registry" module ActiveRecord class ExplainSubscriber # :nodoc: diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 6cf2e01179..f1ea0e022f 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "erb" require "yaml" diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c9e97d9d2b..86f13d75d5 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require "erb" require "yaml" require "zlib" require "set" require "active_support/dependencies" require "active_support/core_ext/digest/uuid" -require_relative "fixture_set/file" -require_relative "errors" +require "active_record/fixture_set/file" +require "active_record/errors" module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -145,7 +147,7 @@ module ActiveRecord # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module # that is included in ActiveRecord::FixtureSet.context_class. # - # - define a helper method in `test_helper.rb` + # - define a helper method in <tt>test_helper.rb</tt> # module FixtureFileHelpers # def file_sha(path) # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) @@ -1063,6 +1065,10 @@ class ActiveRecord::FixtureSet::RenderContext # :nodoc: def get_binding binding() end + + def binary(path) + %(!!binary "#{Base64.strict_encode64(File.read(path))}") + end end end end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 1a937dbcf7..7e47dac016 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt> def self.gem_version @@ -8,7 +10,7 @@ module ActiveRecord MAJOR = 5 MINOR = 2 TINY = 0 - PRE = "alpha" + PRE = "beta2" 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 5776807507..f3fe610c09 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/indifferent_access" module ActiveRecord @@ -30,7 +32,7 @@ module ActiveRecord # for differentiating between them or reloading the right type with find. # # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html # module Inheritance extend ActiveSupport::Concern @@ -45,14 +47,13 @@ module ActiveRecord # Determines if one of the attributes passed in is the inheritance column, # and if the inheritance column is attr accessible, it initializes an # instance of the given subclass instead of the base class. - def new(*args, &block) + def new(attributes = nil, &block) if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end - attrs = args.first if has_attribute?(inheritance_column) - subclass = subclass_from_attributes(attrs) + subclass = subclass_from_attributes(attributes) if subclass.nil? && base_class == self subclass = subclass_from_attributes(column_defaults) @@ -60,7 +61,7 @@ module ActiveRecord end if subclass && subclass != self - subclass.new(*args, &block) + subclass.new(attributes, &block) else super end @@ -245,7 +246,7 @@ module ActiveRecord def ensure_proper_type klass = self.class if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) + _write_attribute(klass.inheritance_column, klass.sti_name) end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index cf954852bc..6cf26a9792 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/filters" module ActiveRecord diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 89e5d153b8..5a65edf27e 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -1,5 +1,7 @@ -require_relative "scoping/default" -require_relative "scoping/named" +# frozen_string_literal: true + +require "active_record/scoping/default" +require "active_record/scoping/named" module ActiveRecord # This class is used to create a table that keeps track of values and keys such diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb index c7683f68c7..ffa095dd94 100644 --- a/activerecord/lib/active_record/legacy_yaml_adapter.rb +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module LegacyYamlAdapter def self.convert(klass, coder) @@ -6,7 +8,7 @@ module ActiveRecord case coder["active_record_yaml_version"] when 1, 2 then coder else - if coder["attributes"].is_a?(AttributeSet) + if coder["attributes"].is_a?(ActiveModel::AttributeSet) Rails420.convert(klass, coder) else Rails41.convert(klass, coder) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 522da6a571..e1e24e2814 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Locking # == What is Optimistic Locking diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 263e2a5f7f..bb85c47e06 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Locking # Locking::Pessimistic provides support for row-level locking using @@ -51,8 +53,8 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html - # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE + # MySQL: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html + # PostgreSQL: https://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested # lock. Pass an SQL locking clause to append the end of the SELECT statement @@ -61,12 +63,13 @@ module ActiveRecord def lock!(lock = true) if persisted? if changed? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Locking a record with unpersisted changes is deprecated and will raise an - exception in Rails 5.2. Use `save` to persist the changes, or `reload` to - discard them explicitly. + raise(<<-MSG.squish) + Locking a record with unpersisted changes is not supported. Use + `save` to persist the changes, or `reload` to discard them + explicitly. MSG end + reload(lock: lock) end self diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index e39ca5f6dc..9234029c22 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] @@ -88,6 +90,47 @@ module ActiveRecord def logger ActiveRecord::Base.logger end + + def debug(progname = nil, &block) + return unless super + + if ActiveRecord::Base.verbose_query_logs + log_query_source + end + end + + def log_query_source + source_line, line_number = extract_callstack(caller_locations) + + if source_line + if defined?(::Rails.root) + app_root = "#{::Rails.root.to_s}/".freeze + source_line = source_line.sub(app_root, "") + end + + logger.debug(" ↳ #{ source_line }:#{ line_number }") + 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__) + "/" + + def ignored_callstack(path) + path.start_with?(RAILS_GEM_ROOT) || + path.start_with?(RbConfig::CONFIG["rubylibdir"]) + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 42220b9a5e..f6648a4e3d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" @@ -352,9 +354,9 @@ module ActiveRecord # to match the structure of your database. # # To roll the database back to a previous migration version, use - # <tt>rails db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which + # <tt>rails db:rollback VERSION=X</tt> where <tt>X</tt> is the version to which # you wish to downgrade. Alternatively, you can also use the STEP option if you - # wish to rollback last few migrations. <tt>rails db:migrate STEP=2</tt> will rollback + # wish to rollback last few migrations. <tt>rails db:rollback STEP=2</tt> will rollback # the latest two migrations. # # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, @@ -579,7 +581,8 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. - FileUtils.cd Rails.root do + root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root + FileUtils.cd(root) do current_config = Base.connection_config Base.clear_all_connections! system("bin/rails db:test:prepare") @@ -731,6 +734,24 @@ module ActiveRecord execute_block { yield helper } end + # Used to specify an operation that is only run when migrating up + # (for example, populating a new column with its initial values). + # + # In the following example, the new column +published+ will be given + # the value +true+ for all existing records. + # + # class AddPublishedToPosts < ActiveRecord::Migration[5.2] + # def change + # add_column :posts, :published, :boolean, default: false + # up_only do + # execute "update posts set published = 'true'" + # end + # end + # end + def up_only + execute_block { yield } unless reverting? + end + # Runs the given migration classes. # Last argument can specify options: # - :direction (default is :up) @@ -1024,12 +1045,7 @@ module ActiveRecord new(:up, migrations(migrations_paths), nil) end - def schema_migrations_table_name - SchemaMigration.table_name - end - deprecate :schema_migrations_table_name - - def get_all_versions(connection = Base.connection) + def get_all_versions if SchemaMigration.table_exists? SchemaMigration.all_versions.map(&:to_i) else @@ -1037,12 +1053,13 @@ module ActiveRecord end end - def current_version(connection = Base.connection) - get_all_versions(connection).max || 0 + def current_version(connection = nil) + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError end - def needs_migration?(connection = Base.connection) - (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 + def needs_migration?(connection = nil) + (migrations(migrations_paths).collect(&:version) - get_all_versions).size > 0 end def any_migrations? @@ -1227,7 +1244,7 @@ module ActiveRecord # Return true if a valid version is not provided. def invalid_target? - !target && @target_version && @target_version > 0 + @target_version && @target_version != 0 && !target end def execute_migration_in_transaction(migration, direction) @@ -1260,10 +1277,10 @@ module ActiveRecord end def validate(migrations) - name , = migrations.group_by(&:name).find { |_, v| v.length > 1 } + name, = migrations.group_by(&:name).find { |_, v| v.length > 1 } raise DuplicateMigrationNameError.new(name) if name - version , = migrations.group_by(&:version).find { |_, v| v.length > 1 } + version, = migrations.group_by(&:version).find { |_, v| v.length > 1 } raise DuplicateMigrationVersionError.new(version) if version end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index f9cf59b283..81ef4828f8 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Migration # <tt>ActiveRecord::Migration::CommandRecorder</tt> records commands done during @@ -108,7 +110,7 @@ module ActiveRecord private - module StraightReversions + module StraightReversions # :nodoc: private { transaction: :transaction, execute_block: :execute_block, @@ -159,8 +161,8 @@ module ActiveRecord table, columns, options = *args options ||= {} - index_name = options[:name] - options_hash = index_name ? { name: index_name } : { column: columns } + options_hash = options.slice(:name, :algorithm) + options_hash[:column] = columns if !options_hash[:name] [:remove_index, [table, options_hash]] end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 188dd0acef..7ae8073478 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Migration module Compatibility # :nodoc: all @@ -14,10 +16,35 @@ module ActiveRecord V5_2 = Current class V5_1 < V5_2 + def change_column(table_name, column_name, type, options = {}) + if adapter_name == "PostgreSQL" + clear_cache! + sql = connection.send(:change_column_sql, table_name, column_name, type, options) + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}" + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + else + super + end + end + + def create_table(table_name, options = {}) + if adapter_name == "Mysql2" + super(table_name, options: "ENGINE=InnoDB", **options) + else + super + end + end end class V5_0 < V5_1 module TableDefinition + def primary_key(name, type = :primary_key, **options) + type = :integer if type == :primary_key + super + end + def references(*args, **options) super(*args, type: :integer, **options) end @@ -37,7 +64,7 @@ module ActiveRecord end end - # Since 5.1 Postgres adapter uses bigserial type for primary + # Since 5.1 PostgreSQL adapter uses bigserial type for primary # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize # serial/int type instead -- the way it used to work before 5.1. unless options.key?(:id) @@ -45,11 +72,8 @@ module ActiveRecord end if block_given? - super(table_name, options) do |t| - class << t - prepend TableDefinition - end - yield t + super do |t| + yield compatible_table_definition(t) end else super @@ -58,21 +82,46 @@ module ActiveRecord def change_table(table_name, options = {}) if block_given? - super(table_name, options) do |t| - class << t - prepend TableDefinition - end - yield t + super do |t| + yield compatible_table_definition(t) + end + else + super + end + end + + def create_join_table(table_1, table_2, column_options: {}, **options) + column_options.reverse_merge!(type: :integer) + + if block_given? + super do |t| + yield compatible_table_definition(t) end else super end end + def add_column(table_name, column_name, type, options = {}) + if type == :primary_key + type = :integer + options[:primary_key] = true + end + super + end + def add_reference(table_name, ref_name, **options) super(table_name, ref_name, type: :integer, **options) end alias :add_belongs_to :add_reference + + private + def compatible_table_definition(t) + class << t + prepend TableDefinition + end + t + end end class V4_2 < V5_0 @@ -91,11 +140,8 @@ module ActiveRecord def create_table(table_name, options = {}) if block_given? - super(table_name, options) do |t| - class << t - prepend TableDefinition - end - yield t + super do |t| + yield compatible_table_definition(t) end else super @@ -104,11 +150,8 @@ module ActiveRecord def change_table(table_name, options = {}) if block_given? - super(table_name, options) do |t| - class << t - prepend TableDefinition - end - yield t + super do |t| + yield compatible_table_definition(t) end else super @@ -144,6 +187,12 @@ module ActiveRecord end private + def compatible_table_definition(t) + class << t + prepend TableDefinition + end + super + end def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 89789f00ea..9abb289bb0 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Migration module JoinTable #:nodoc: diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 14e0f5bff7..fa7537c1a3 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "monitor" module ActiveRecord @@ -85,19 +87,6 @@ module ActiveRecord # Sets the name of the internal metadata table. ## - # :singleton-method: protected_environments - # :call-seq: protected_environments - # - # The array of names of environments where destructive actions should be prohibited. By default, - # the value is <tt>["production"]</tt>. - - ## - # :singleton-method: protected_environments= - # :call-seq: protected_environments=(environments) - # - # Sets an array of names of environments where destructive actions should be prohibited. - - ## # :singleton-method: pluralize_table_names # :call-seq: pluralize_table_names # @@ -113,20 +102,6 @@ module ActiveRecord # If true, the default table name for a Product class will be "products". If false, it would just be "product". # See table_name for the full rules on table/class naming. This is true, by default. - ## - # :singleton-method: ignored_columns - # :call-seq: ignored_columns - # - # The list of columns names the model should ignore. Ignored columns won't have attribute - # accessors defined, and won't be referenced in SQL queries. - - ## - # :singleton-method: ignored_columns= - # :call-seq: ignored_columns=(columns) - # - # Sets the columns names the model should ignore. Ignored columns won't have attribute - # accessors defined, and won't be referenced in SQL queries. - included do mattr_accessor :primary_key_prefix_type, instance_writer: false @@ -134,11 +109,11 @@ module ActiveRecord class_attribute :table_name_suffix, instance_writer: false, default: "" class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" - class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] class_attribute :pluralize_table_names, instance_writer: false, default: true - class_attribute :ignored_columns, instance_accessor: false, default: [].freeze + self.protected_environments = ["production"] self.inheritance_column = "type" + self.ignored_columns = [].freeze delegate :type_for_attribute, to: :class @@ -250,6 +225,21 @@ module ActiveRecord (parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix end + # The array of names of environments where destructive actions should be prohibited. By default, + # the value is <tt>["production"]</tt>. + def protected_environments + if defined?(@protected_environments) + @protected_environments + else + superclass.protected_environments + end + end + + # Sets an array of names of environments where destructive actions should be prohibited. + def protected_environments=(environments) + @protected_environments = environments.map(&:to_s) + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -269,6 +259,22 @@ module ActiveRecord @explicit_inheritance_column = true end + # The list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + def ignored_columns + if defined?(@ignored_columns) + @ignored_columns + else + superclass.ignored_columns + end + end + + # Sets the columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + def ignored_columns=(columns) + @ignored_columns = columns.map(&:to_s) + end + def sequence_name if base_class == self @sequence_name ||= reset_sequence_name @@ -319,11 +325,11 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name| - unless columns_hash.key?(name) - _default_attributes[name].dup - end + unless defined?(@attributes_builder) && @attributes_builder + defaults = _default_attributes.except(*(column_names - [primary_key])) + @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults) end + @attributes_builder end def columns_hash # :nodoc: @@ -342,7 +348,7 @@ module ActiveRecord end def yaml_encoder # :nodoc: - @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types) + @yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types) end # Returns the type of the attribute with the given name, after applying @@ -372,7 +378,7 @@ module ActiveRecord end def _default_attributes # :nodoc: - @default_attributes ||= AttributeSet.new({}) + @default_attributes ||= ActiveModel::AttributeSet.new({}) end # Returns an array of column names as strings. @@ -419,7 +425,7 @@ module ActiveRecord # end def reset_column_information connection.clear_cache! - undefine_attribute_methods + ([self] + descendants).each(&:undefine_attribute_methods) connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache @@ -467,7 +473,6 @@ module ActiveRecord end def reload_schema_from_cache - @arel_engine = nil @arel_table = nil @column_names = nil @attribute_types = nil diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 917bc76993..fa20bce3a9 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/except" +require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/object/try" require "active_support/core_ext/hash/indifferent_access" @@ -60,6 +63,18 @@ module ActiveRecord # member.update params[:member] # member.avatar.icon # => 'sad' # + # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option. + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar, update_only: true + # end + # + # params = { member: { avatar_attributes: { icon: 'sad' } } } + # member.update params[:member] + # member.avatar.id # => 2 + # member.avatar.icon # => 'sad' + # # By default you will only be able to set and update attributes on the # associated model. If you want to destroy the associated model through the # attributes hash, you have to enable it first using the @@ -353,9 +368,7 @@ module ActiveRecord # associations are just regular associations. def generate_association_writer(association_name, type) generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 - if method_defined?(:#{association_name}_attributes=) - remove_method(:#{association_name}_attributes=) - end + silence_redefinition_of_method :#{association_name}_attributes= def #{association_name}_attributes=(attributes) assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) end diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index 4059020e25..754c891884 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record No Touching module NoTouching extend ActiveSupport::Concern module ClassMethods - # Lets you selectively disable calls to `touch` for the + # Lets you selectively disable calls to +touch+ for the # duration of a block. # # ==== Examples diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 26966f9433..cf0de0fdeb 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module NullRelation # :nodoc: def pluck(*column_names) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b2dba5516e..462e5e7aaf 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record \Persistence module Persistence @@ -69,6 +71,132 @@ module ActiveRecord klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) end + # Updates an object (or multiple objects) and saves it to the database, if validations pass. + # The resulting object is returned whether the object was saved successfully to the database or not. + # + # ==== Parameters + # + # * +id+ - This should be the id or an array of ids to be updated. + # * +attributes+ - This should be a hash of attributes or an array of hashes. + # + # ==== Examples + # + # # Updates one record + # Person.update(15, user_name: "Samuel", group: "expert") + # + # # Updates multiple records + # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } + # Person.update(people.keys, people.values) + # + # # Updates multiple records from the result of a relation + # people = Person.where(group: "expert") + # people.update(group: "masters") + # + # Note: Updating a large number of records will run an UPDATE + # query for each record, which may cause a performance issue. + # 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) + 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, + "You are passing an instance of ActiveRecord::Base to `update`. " \ + "Please pass the id of the object by calling `.id`." + end + object = find(id) + object.update(attributes) + object + end + end + + # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, + # therefore all callbacks and filters are fired off before the object is deleted. This method is + # less efficient than #delete but allows cleanup methods and other actions to be run. + # + # This essentially finds the object (or multiple objects) with the given id, creates a new object + # from the attributes, and then calls destroy on it. + # + # ==== Parameters + # + # * +id+ - This should be the id or an array of ids to be destroyed. + # + # ==== Examples + # + # # Destroy a single object + # Todo.destroy(1) + # + # # Destroy multiple objects + # todos = [1,2,3] + # Todo.destroy(todos) + def destroy(id) + if id.is_a?(Array) + find(id).each(&:destroy) + else + find(id).destroy + end + end + + # Deletes the row with a primary key matching the +id+ argument, using a + # SQL +DELETE+ statement, and returns the number of rows deleted. Active + # Record objects are not instantiated, so the object's callbacks are not + # executed, including any <tt>:dependent</tt> association options. + # + # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. + # + # Note: Although it is often much faster than the alternative, #destroy, + # skipping callbacks might bypass business logic in your application + # that ensures referential integrity or performs other essential jobs. + # + # ==== Examples + # + # # Delete a single row + # Todo.delete(1) + # + # # Delete multiple rows + # Todo.delete([2,3,4]) + def delete(id_or_array) + where(primary_key => id_or_array).delete_all + end + + def _insert_record(values) # :nodoc: + primary_key_value = nil + + if primary_key && Hash === values + arel_primary_key = arel_attribute(primary_key) + primary_key_value = values[arel_primary_key] + + if !primary_key_value && prefetch_primary_key? + primary_key_value = next_sequence_value + values[arel_primary_key] = primary_key_value + end + end + + if values.empty? + im = arel_table.compile_insert(connection.empty_insert_statement_value) + im.into arel_table + else + im = arel_table.compile_insert(_substitute_values(values)) + end + + 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) + um = arel_table.where( + arel_attribute(primary_key).eq(bind) + ).compile_update(_substitute_values(values), primary_key) + + connection.update(um, "#{self} Update") + end + private # Called by +instantiate+ to decide which class to use for a new # record instance. @@ -78,6 +206,13 @@ module ActiveRecord def discriminate_class_for_record(record) self end + + def _substitute_values(values) + values.map do |attr, value| + bind = predicate_builder.build_bind_attribute(attr.name, value) + [attr, bind] + end + end end # Returns true if this object hasn't been saved yet -- that is, a record @@ -175,7 +310,7 @@ module ActiveRecord # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - self.class.delete(id) if persisted? + _relation_for_itself.delete_all if persisted? @destroyed = true freeze end @@ -226,7 +361,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) + became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -267,11 +402,7 @@ module ActiveRecord verify_readonly_attribute(name) public_send("#{name}=", value) - if has_changes_to_save? - save(validate: false) - else - true - end + save(validate: false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -330,10 +461,10 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end - updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes) + updated_count = _relation_for_itself.update_all(attributes) attributes.each do |k, v| - raw_write_attribute(k, v) + write_attribute_without_type_cast(k, v) end updated_count == 1 @@ -351,7 +482,7 @@ module ActiveRecord # Wrapper around #increment that writes the update to the database. # Only +attribute+ is updated; the record itself is not saved. # This means that any other modified attributes will still be dirty. - # Validations and callbacks are skipped. Supports the `touch` option from + # Validations and callbacks are skipped. Supports the +touch+ option from # +update_counters+, see that for more. # Returns +self+. def increment!(attribute, by = 1, touch: nil) @@ -372,7 +503,7 @@ module ActiveRecord # Wrapper around #decrement that writes the update to the database. # Only +attribute+ is updated; the record itself is not saved. # This means that any other modified attributes will still be dirty. - # Validations and callbacks are skipped. Supports the `touch` option from + # Validations and callbacks are skipped. Supports the +touch+ option from # +update_counters+, see that for more. # Returns +self+. def decrement!(attribute, by = 1, touch: nil) @@ -521,8 +652,7 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - primary_key = self.class.primary_key - scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) + scope = _relation_for_itself if locking_enabled? locking_column = self.class.locking_column @@ -555,6 +685,10 @@ module ActiveRecord end def relation_for_destroy + _relation_for_itself + end + + def _relation_for_itself self.class.unscoped.where(self.class.primary_key => id) end @@ -572,7 +706,7 @@ module ActiveRecord rows_affected = 0 @_trigger_update_callback = true else - rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database + rows_affected = self.class._update_record(attributes_values, id, id_in_database) @_trigger_update_callback = rows_affected > 0 end @@ -586,7 +720,7 @@ module ActiveRecord def _create_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_create(attribute_names) - new_id = self.class.unscoped.insert attributes_values + new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index e4c2e1f86f..8e23128333 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record Query Cache class QueryCache @@ -24,16 +26,19 @@ module ActiveRecord end def self.run - caching_pool = ActiveRecord::Base.connection_pool - caching_was_enabled = caching_pool.query_cache_enabled + ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool| + caching_was_enabled = pool.query_cache_enabled - caching_pool.enable_query_cache! + pool.enable_query_cache! - [caching_pool, caching_was_enabled] + [pool, caching_was_enabled] + end end - def self.complete((caching_pool, caching_was_enabled)) - caching_pool.disable_query_cache! unless caching_was_enabled + def self.complete(caching_pools) + caching_pools.each do |pool, caching_was_enabled| + pool.disable_query_cache! unless caching_was_enabled + end 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 b16e178358..3996d5661f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all @@ -5,7 +7,7 @@ module ActiveRecord 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_by, :find_by!, to: :all - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all + delegate :destroy_all, :delete_all, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 9cca103a18..4538ed6a5f 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record" require "rails" require "active_model/railtie" @@ -26,6 +28,9 @@ module ActiveRecord config.active_record.use_schema_cache_dump = true config.active_record.maintain_test_schema = true + config.active_record.sqlite3 = ActiveSupport::OrderedOptions.new + config.active_record.sqlite3.represent_boolean_as_integer = nil + config.eager_load_namespaces << ActiveRecord rake_tasks do @@ -48,8 +53,8 @@ module ActiveRecord # to avoid cross references when loading a constant for the # first time. Also, make it output to STDERR. console do |app| - require_relative "railties/console_sandbox" if app.sandbox? - require_relative "base" + require "active_record/railties/console_sandbox" if app.sandbox? + require "active_record/base" unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console @@ -57,7 +62,7 @@ module ActiveRecord end runner do - require_relative "base" + require "active_record/base" end initializer "active_record.initialize_timezone" do @@ -85,12 +90,15 @@ module ActiveRecord filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) + current_version = ActiveRecord::Migrator.current_version + next if current_version.nil? + cache = YAML.load(File.read(filename)) - if cache.version == ActiveRecord::Migrator.current_version + if cache.version == current_version connection.schema_cache = cache connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." + warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}." end end end @@ -101,14 +109,16 @@ module ActiveRecord initializer "active_record.warn_on_records_fetched_greater_than" do if config.active_record.warn_on_records_fetched_greater_than ActiveSupport.on_load(:active_record) do - require_relative "relation/record_fetch_warning" + require "active_record/relation/record_fetch_warning" end end end initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - app.config.active_record.each do |k, v| + configs = app.config.active_record.dup + configs.delete(:sqlite3) + configs.each do |k, v| send "#{k}=", v end end @@ -139,7 +149,7 @@ end_warning # Expose database runtime to controller for logging. initializer "active_record.log_runtime" do - require_relative "railties/controller_runtime" + require "active_record/railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime end @@ -170,7 +180,45 @@ end_warning initializer "active_record.clear_active_connections" do config.after_initialize do ActiveSupport.on_load(:active_record) do + # Ideally the application doesn't connect to the database during boot, + # but sometimes it does. In case it did, we want to empty out the + # connection pools so that a non-database-using process (e.g. a master + # process in a forking server model) doesn't retain a needless + # connection. If it was needed, the incremental cost of reestablishing + # this connection is trivial: the rest of the pool would need to be + # populated anyway. + clear_active_connections! + flush_idle_connections! + end + end + end + + initializer "active_record.check_represent_sqlite3_boolean_as_integer" do + config.after_initialize do + ActiveSupport.on_load(:active_record_sqlite3adapter) do + represent_boolean_as_integer = Rails.application.config.active_record.sqlite3.delete(:represent_boolean_as_integer) + unless represent_boolean_as_integer.nil? + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer + end + + unless ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + ActiveSupport::Deprecation.warn <<-MSG +Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` +set to false is deprecated. SQLite databases have used 't' and 'f' to serialize +boolean values and must have old data converted to 1 and 0 (its native boolean +serialization) before setting this flag to true. Conversion can be accomplished +by setting up a rake task which runs + + ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) + ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) + +for all models and all boolean columns, after which the flag must be set to +true by adding the following to your application.rb file: + + Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true +MSG + end end end end diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 604a220303..8917638a5d 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Base.connection.begin_transaction(joinable: false) at_exit do diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 4030cdc158..2ae733f657 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "active_support/core_ext/module/attr_internal" -require_relative "../log_subscriber" +require "active_record/log_subscriber" module ActiveRecord module Railties # :nodoc: diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index abc7323341..fce3e1c5cf 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,17 +1,19 @@ +# frozen_string_literal: true + require "active_record" db_namespace = namespace :db do desc "Set the environment value for the database" - task "environment:set" => [:environment, :load_config] do + task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end - task check_protected_environments: [:environment, :load_config] do + task check_protected_environments: :load_config do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end - task :load_config do + task load_config: :environment do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end @@ -54,7 +56,7 @@ db_namespace = namespace :db do end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." - task migrate: [:environment, :load_config] do + task migrate: :load_config do ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace["_dump"].invoke end @@ -76,7 +78,7 @@ db_namespace = namespace :db do namespace :migrate do # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' - task redo: [:environment, :load_config] do + task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? if ENV["VERSION"] @@ -92,24 +94,35 @@ db_namespace = namespace :db do task reset: ["db:drop", "db:create", "db:migrate"] # desc 'Runs the "up" for a given migration VERSION.' - task up: [:environment, :load_config] do + task up: :load_config do raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? - version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) + ActiveRecord::Tasks::DatabaseTasks.check_target_version + + ActiveRecord::Migrator.run( + :up, + ActiveRecord::Tasks::DatabaseTasks.migrations_paths, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' - task down: [:environment, :load_config] do + task down: :load_config do raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? - version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) + + ActiveRecord::Tasks::DatabaseTasks.check_target_version + + ActiveRecord::Migrator.run( + :down, + ActiveRecord::Tasks::DatabaseTasks.migrations_paths, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) db_namespace["_dump"].invoke end desc "Display status of migrations" - task status: [:environment, :load_config] do + task status: :load_config do unless ActiveRecord::SchemaMigration.table_exists? abort "Schema migrations table does not exist yet." end @@ -127,14 +140,14 @@ db_namespace = namespace :db do end desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." - task rollback: [:environment, :load_config] do + task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' - task forward: [:environment, :load_config] do + task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace["_dump"].invoke @@ -144,12 +157,12 @@ db_namespace = namespace :db do task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" - task charset: [:environment, :load_config] do + task charset: :load_config do puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task collation: [:environment, :load_config] do + task collation: :load_config do begin puts ActiveRecord::Tasks::DatabaseTasks.collation_current rescue NoMethodError @@ -158,12 +171,12 @@ db_namespace = namespace :db do end desc "Retrieves the current schema version number" - task version: [:environment, :load_config] do + task version: :load_config do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task abort_if_pending_migrations: [:environment, :load_config] do + task abort_if_pending_migrations: :load_config do pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations if pending_migrations.any? @@ -186,8 +199,8 @@ db_namespace = namespace :db do namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task load: [:environment, :load_config] do - require_relative "../fixtures" + task load: :load_config do + require "active_record/fixtures" base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -208,8 +221,8 @@ db_namespace = namespace :db do end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task identify: [:environment, :load_config] do - require_relative "../fixtures" + task identify: :load_config do + require "active_record/fixtures" label, id = ENV["LABEL"], ENV["ID"] raise "LABEL or ID required" if label.blank? && id.blank? @@ -234,8 +247,8 @@ db_namespace = namespace :db do namespace :schema do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" - task dump: [:environment, :load_config] do - require_relative "../schema_dumper" + 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) @@ -244,7 +257,7 @@ db_namespace = namespace :db do end desc "Loads a schema.rb file into the database" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"]) end @@ -254,14 +267,14 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." - task dump: [:environment, :load_config] do + task dump: :load_config do conn = ActiveRecord::Base.connection filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename) end desc "Clears a db/schema_cache.yml file." - task clear: [:environment, :load_config] do + task clear: :load_config do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") rm_f filename, verbose: false end @@ -271,7 +284,7 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" - task dump: [:environment, :load_config] do + 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) @@ -286,7 +299,7 @@ db_namespace = namespace :db do end desc "Recreates the databases from the structure.sql file" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"]) end @@ -311,7 +324,7 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test" ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -321,16 +334,16 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task load_structure: %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test" end # desc "Empty the test database" - task purge: %w(environment load_config check_protected_environments) do + task purge: %w(load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] end # desc 'Load the test schema' - task prepare: %w(environment load_config) do + task prepare: :load_config do unless ActiveRecord::Base.configurations.blank? db_namespace["test:load"].invoke end diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb deleted file mode 100644 index d7cf4df339..0000000000 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ /dev/null @@ -1,16 +0,0 @@ -#FIXME Remove if ArJdbcMysql will give. -module ArJdbcMySQL #:nodoc: - class Error < StandardError #:nodoc: - attr_accessor :error_number, :sql_state - - def initialize(msg) - super - @error_number = nil - @sql_state = nil - end - - # Mysql gem compatibility - alias_method :errno, :error_number - alias_method :error, :message - end -end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index af6473d250..7bc26993d5 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a453ca55c7..9e32b69786 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,6 +1,7 @@ -require "thread" +# frozen_string_literal: true + require "active_support/core_ext/string/filters" -require "active_support/deprecation" +require "concurrent/map" module ActiveRecord # = Active Record Reflection @@ -33,7 +34,8 @@ module ActiveRecord def self.add_reflection(ar, name, reflection) ar.clear_reflections_cache - ar._reflections = ar._reflections.merge(name.to_s => reflection) + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -136,7 +138,7 @@ module ActiveRecord # HasAndBelongsToManyReflection # ThroughReflection # PolymorphicReflection - # RuntimeReflection + # RuntimeReflection class AbstractReflection # :nodoc: def through_reflection? false @@ -152,14 +154,6 @@ module ActiveRecord klass.new(attributes, &block) end - def quoted_table_name - klass.quoted_table_name - end - - def primary_key_type - klass.type_for_attribute(klass.primary_key) - end - # Returns the class name for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> @@ -180,11 +174,6 @@ module ActiveRecord scope ? [scope] : [] end - def scope_chain - chain.map(&:scopes) - end - deprecate :scope_chain - def build_join_constraint(table, foreign_table) key = join_keys.key foreign_key = join_keys.foreign_key @@ -212,20 +201,15 @@ module ActiveRecord def join_scopes(table, predicate_builder) # :nodoc: if scope - [build_scope(table, predicate_builder).instance_exec(&scope)] + [scope_for(build_scope(table, predicate_builder))] else [] end end def klass_join_scope(table, predicate_builder) # :nodoc: - if klass.current_scope - klass.current_scope.clone.tap { |scope| - scope.joins_values = scope.left_outer_joins_values = [].freeze - } - else - klass.default_scoped(build_scope(table, predicate_builder)) - end + relation = build_scope(table, predicate_builder) + klass.scope_for_association(relation) end def constraints @@ -303,13 +287,21 @@ module ActiveRecord end def get_join_keys(association_klass) - JoinKeys.new(join_pk(association_klass), join_fk) + JoinKeys.new(join_primary_key(association_klass), join_foreign_key) end def build_scope(table, predicate_builder = predicate_builder(table)) Relation.create(klass, table, predicate_builder) end + def join_primary_key(*) + foreign_key + end + + def join_foreign_key + active_record_primary_key + end + protected def actual_source_reflection # FIXME: this is a horrible name self @@ -320,12 +312,8 @@ module ActiveRecord PredicateBuilder.new(TableMetadata.new(klass, table)) end - def join_pk(_) - foreign_key - end - - def join_fk - active_record_primary_key + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end end @@ -372,6 +360,17 @@ module ActiveRecord # # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class + # + # class Company < ActiveRecord::Base + # has_many :clients + # end + # + # Company.reflect_on_association(:clients).klass + # # => Client + # + # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate + # a new association object. Use +build_association+ or +create_association+ + # instead. This allows plugins to hook into association object creation. def klass @klass ||= compute_class(class_name) end @@ -390,8 +389,8 @@ module ActiveRecord active_record == other_aggregation.active_record end - def scope_for(klass) - scope ? klass.unscoped.instance_exec(nil, &scope) : klass.unscoped + def scope_for(relation, owner = nil) + relation.instance_exec(owner, &scope) || relation end private @@ -412,22 +411,6 @@ module ActiveRecord # Holds all the metadata about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: - # Returns the target association's class. - # - # class Author < ActiveRecord::Base - # has_many :books - # end - # - # Author.reflect_on_association(:books).klass - # # => Book - # - # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate - # a new association object. Use +build_association+ or +create_association+ - # instead. This allows plugins to hook into association object creation. - def klass - @klass ||= compute_class(class_name) - end - def compute_class(name) active_record.send(:compute_type, name) end @@ -437,33 +420,22 @@ module ActiveRecord def initialize(name, scope, options, active_record) super - @automatic_inverse_of = nil @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") - @foreign_type = options[:foreign_type] || "#{name}_type" + @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type") @constructable = calculate_constructable(macro, options) - @association_scope_cache = {} - @scope_lock = Mutex.new + @association_scope_cache = Concurrent::Map.new if options[:class_name] && options[:class_name].class == Class - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class to the `class_name` is deprecated and will raise - an ArgumentError in Rails 5.2. It eagerloads more classes than - necessary and potentially creates circular dependencies. - - Please pass the class name as a string: - `#{macro} :#{name}, class_name: '#{options[:class_name]}'` - MSG + raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string." end end - def association_scope_cache(conn, owner) + def association_scope_cache(conn, owner, &block) key = conn.prepared_statements if polymorphic? key = [key, owner._read_attribute(@foreign_type)] end - @association_scope_cache[key] ||= @scope_lock.synchronize { - @association_scope_cache[key] ||= yield - } + @association_scope_cache.compute_if_absent(key) { StatementCache.create(conn, &block) } end def constructable? # :nodoc: @@ -487,10 +459,6 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end @@ -513,7 +481,7 @@ module ActiveRecord alias :check_eager_loadable! :check_preloadable! def join_id_for(owner) # :nodoc: - owner[active_record_primary_key] + owner[join_foreign_key] end def through_reflection @@ -624,12 +592,14 @@ module ActiveRecord # If it cannot find a suitable inverse association name, it returns # +nil+. def inverse_name - options.fetch(:inverse_of) do - @automatic_inverse_of ||= automatic_inverse_of + unless defined?(@inverse_name) + @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of } end + + @inverse_name end - # returns either false or the inverse association name that it finds. + # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym @@ -646,8 +616,6 @@ module ActiveRecord return inverse_name end end - - false end # Checks if the inverse reflection that is returned from the @@ -659,7 +627,7 @@ module ActiveRecord # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && - klass.name == reflection.active_record.name && + klass <= reflection.active_record && can_find_inverse_of_automatically?(reflection) end @@ -698,10 +666,6 @@ module ActiveRecord def derive_join_table ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end - - def primary_key(klass) - klass.primary_key || raise(UnknownPrimaryKey.new(klass)) - end end class HasManyReflection < AssociationReflection # :nodoc: @@ -716,6 +680,10 @@ module ActiveRecord Associations::HasManyAssociation end end + + def association_primary_key(klass = nil) + primary_key(klass || self.klass) + end end class HasOneReflection < AssociationReflection # :nodoc: @@ -751,8 +719,12 @@ module ActiveRecord end end - def join_id_for(owner) # :nodoc: - owner[foreign_key] + def join_primary_key(klass = nil) + polymorphic? ? association_primary_key(klass) : association_primary_key + end + + def join_foreign_key + foreign_key end private @@ -760,14 +732,6 @@ module ActiveRecord def calculate_constructable(macro, options) !polymorphic? end - - def join_fk - foreign_key - end - - def join_pk(klass) - polymorphic? ? association_primary_key(klass) : association_primary_key - end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -781,7 +745,7 @@ module ActiveRecord # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - delegate :foreign_key, :foreign_type, :association_foreign_key, + delegate :foreign_key, :foreign_type, :association_foreign_key, :join_id_for, :active_record_primary_key, :type, :get_join_keys, to: :source_reflection def initialize(delegate_reflection) @@ -872,10 +836,6 @@ module ActiveRecord source_reflection.join_scopes(table, predicate_builder) + super end - def source_type_scope - through_reflection.klass.where(foreign_type => options[:source_type]) - end - def has_scope? scope || options[:source_type] || source_reflection.has_scope? || @@ -896,10 +856,6 @@ module ActiveRecord actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base @@ -944,10 +900,6 @@ module ActiveRecord through_reflection.options end - def join_id_for(owner) # :nodoc: - source_reflection.join_id_for(owner) - end - def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) @@ -1025,10 +977,6 @@ module ActiveRecord end end - def primary_key(klass) - klass.primary_key || raise(UnknownPrimaryKey.new(klass)) - end - def inverse_name; delegate_reflection.send(:inverse_name); end def derive_class_name @@ -1043,66 +991,32 @@ module ActiveRecord end class PolymorphicReflection < AbstractReflection # :nodoc: + delegate :klass, :scope, :plural_name, :type, :get_join_keys, :scope_for, to: :@reflection + def initialize(reflection, previous_reflection) @reflection = reflection @previous_reflection = previous_reflection end - def scopes - scopes = @previous_reflection.scopes - if @previous_reflection.options[:source_type] - scopes + [@previous_reflection.source_type_scope] - else - scopes - end - end - def join_scopes(table, predicate_builder) # :nodoc: scopes = @previous_reflection.join_scopes(table, predicate_builder) + super - if @previous_reflection.options[:source_type] - scopes + [@previous_reflection.source_type_scope] - else - scopes - end - end - - def klass - @reflection.klass - end - - def scope - @reflection.scope - end - - def table_name - @reflection.table_name - end - - def plural_name - @reflection.plural_name - end - - def type - @reflection.type + scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope) end def constraints - @reflection.constraints + [source_type_info] - end - - def source_type_info - type = @previous_reflection.foreign_type - source_type = @previous_reflection.options[:source_type] - lambda { |object| where(type => source_type) } + @reflection.constraints + [source_type_scope] end - def get_join_keys(association_klass) - @reflection.get_join_keys(association_klass) - end + private + def source_type_scope + type = @previous_reflection.foreign_type + source_type = @previous_reflection.options[:source_type] + lambda { |object| where(type => source_type) } + end end - class RuntimeReflection < PolymorphicReflection # :nodoc: - attr_accessor :next + class RuntimeReflection < AbstractReflection # :nodoc: + delegate :scope, :type, :constraints, :get_join_keys, to: :@reflection def initialize(reflection, association) @reflection = reflection @@ -1113,24 +1027,8 @@ module ActiveRecord @association.klass end - def table_name - klass.table_name - end - - def constraints - @reflection.constraints - end - - def source_type_info - @reflection.source_type_info - end - - def alias_candidate(name) - "#{plural_name}_#{name}_join" - end - - def alias_name - Arel::Table.new(table_name, type_caster: klass.type_caster) + def aliased_table + @aliased_table ||= Arel::Table.new(table_name, type_caster: klass.type_caster) end def all_includes; yield; end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 52f5d5f3e3..4df3864d07 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record \Relation class Relation @@ -6,9 +8,9 @@ module ActiveRecord :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with] + :reverse_order, :distinct, :create_with, :skip_query_cache] CLAUSE_METHODS = [:where, :having, :from] - INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] + INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS @@ -34,74 +36,6 @@ module ActiveRecord reset end - def insert(values) # :nodoc: - primary_key_value = nil - - if primary_key && Hash === values - primary_key_value = values[values.keys.find { |k| - k.name == primary_key - }] - - if !primary_key_value && klass.prefetch_primary_key? - primary_key_value = klass.next_sequence_value - values[arel_attribute(klass.primary_key)] = primary_key_value - end - end - - im = arel.create_insert - im.into @table - - substitutes, binds = substitute_values values - - if values.empty? # empty insert - im.values = Arel.sql(connection.empty_insert_statement_value) - else - im.insert substitutes - end - - @klass.connection.insert( - im, - "SQL", - primary_key || false, - primary_key_value, - nil, - binds) - end - - def _update_record(values, id, id_was) # :nodoc: - substitutes, binds = substitute_values values - - scope = @klass.unscoped - - if @klass.finder_needs_type_condition? - scope.unscope!(where: @klass.inheritance_column) - end - - relation = scope.where(@klass.primary_key => (id_was || id)) - bvs = binds + relation.bound_attributes - um = relation - .arel - .compile_update(substitutes, @klass.primary_key) - - @klass.connection.update( - um, - "SQL", - bvs, - ) - end - - def substitute_values(values) # :nodoc: - binds = [] - substitutes = [] - - values.each do |arel_attr, value| - binds.push QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) - substitutes.push [arel_attr, Arel::Nodes::BindParam.new] - end - - [substitutes, binds] - end - def arel_attribute(name) # :nodoc: klass.arel_attribute(name, table) end @@ -118,8 +52,8 @@ module ActiveRecord # # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar - def new(*args, &block) - scoping { @klass.new(*args, &block) } + def new(attributes = nil, &block) + scoping { klass.new(scope_for_create(attributes), &block) } end alias build new @@ -143,8 +77,12 @@ module ActiveRecord # # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> - def create(*args, &block) - scoping { @klass.create(*args, &block) } + def create(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, &block) } + else + scoping { klass.create(scope_for_create(attributes), &block) } + end end # Similar to #create, but calls @@ -153,8 +91,12 @@ module ActiveRecord # # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. - def create!(*args, &block) - scoping { @klass.create!(*args, &block) } + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + scoping { klass.create!(scope_for_create(attributes), &block) } + end end def first_or_create(attributes = nil, &block) # :nodoc: @@ -248,9 +190,10 @@ module ActiveRecord end # Converts relation objects to Array. - def to_a + def to_ary records.dup end + alias to_a to_ary def records # :nodoc: load @@ -364,12 +307,17 @@ module ActiveRecord def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? + if eager_loading? + relation = apply_join_dependency + return relation.update_all(updates) + end + stmt = Arel::UpdateManager.new - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates)) stmt.table(table) - if has_join_values? + if has_join_values? || offset_value @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) @@ -378,51 +326,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - @klass.connection.update stmt, "SQL", bound_attributes - end - - # Updates an object (or multiple objects) and saves it to the database, if validations pass. - # The resulting object is returned whether the object was saved successfully to the database or not. - # - # ==== Parameters - # - # * +id+ - This should be the id or an array of ids to be updated. - # * +attributes+ - This should be a hash of attributes or an array of hashes. - # - # ==== Examples - # - # # Updates one record - # Person.update(15, user_name: 'Samuel', group: 'expert') - # - # # Updates multiple records - # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } - # Person.update(people.keys, people.values) - # - # # Updates multiple records from the result of a relation - # people = Person.where(group: 'expert') - # people.update(group: 'masters') - # - # Note: Updating a large number of records will run an - # UPDATE query for each record, which may cause a performance - # issue. When running callbacks is not needed for each record update, - # it is preferred to use #update_all for updating all records - # in a single query. - def update(id = :all, attributes) - if id.is_a?(Array) - id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } - elsif id == :all - records.each { |record| record.update(attributes) } - else - if ActiveRecord::Base === id - raise ArgumentError, <<-MSG.squish - You are passing an instance of ActiveRecord::Base to `update`. - Please pass the id of the object by calling `.id`. - MSG - end - object = find(id) - object.update(attributes) - object - end + @klass.connection.update stmt, "#{@klass} Update All" end # Destroys the records by instantiating each @@ -445,33 +349,6 @@ module ActiveRecord records.each(&:destroy).tap { reset } end - # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, - # therefore all callbacks and filters are fired off before the object is deleted. This method is - # less efficient than #delete but allows cleanup methods and other actions to be run. - # - # This essentially finds the object (or multiple objects) with the given id, creates a new object - # from the attributes, and then calls destroy on it. - # - # ==== Parameters - # - # * +id+ - Can be either an Integer or an Array of Integers. - # - # ==== Examples - # - # # Destroy a single object - # Todo.destroy(1) - # - # # Destroy multiple objects - # todos = [1,2,3] - # Todo.destroy(todos) - def destroy(id) - if id.is_a?(Array) - id.map { |one_id| destroy(one_id) } - else - find(id).destroy - end - end - # Deletes the records without instantiating the records # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy] # method nor invoking callbacks. @@ -488,8 +365,8 @@ module ActiveRecord # # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # - # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit + # Post.distinct.delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = get_value(method) @@ -499,44 +376,26 @@ module ActiveRecord raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end + if eager_loading? + relation = apply_join_dependency + return relation.delete_all + end + stmt = Arel::DeleteManager.new stmt.from(table) - if has_join_values? + if has_join_values? || has_limit_or_offset? @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else stmt.wheres = arel.constraints end - affected = @klass.connection.delete(stmt, "SQL", bound_attributes) + affected = @klass.connection.delete(stmt, "#{@klass} Destroy") reset affected end - # Deletes the row with a primary key matching the +id+ argument, using a - # SQL +DELETE+ statement, and returns the number of rows deleted. Active - # Record objects are not instantiated, so the object's callbacks are not - # executed, including any <tt>:dependent</tt> association options. - # - # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. - # - # Note: Although it is often much faster than the alternative, - # #destroy, skipping callbacks might bypass business logic in - # your application that ensures referential integrity or performs other - # essential jobs. - # - # ==== Examples - # - # # Delete a single row - # Todo.delete(1) - # - # # Delete multiple rows - # Todo.delete([2,3,4]) - def delete(id_or_array) - where(primary_key => id_or_array).delete_all - end - # Causes the records to be loaded from the database if they have not # been loaded already. You can use this if for some reason you need # to explicitly load some records before actually using them. The @@ -556,8 +415,7 @@ module ActiveRecord end def reset - @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil - @should_eager_load = @join_dependency = nil + @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} self @@ -572,12 +430,12 @@ module ActiveRecord relation = self if eager_loading? - find_with_associations { |rel| relation = rel } + find_with_associations { |rel, _| relation = rel } end conn = klass.connection conn.unprepared_statement { - conn.to_sql(relation.arel, relation.bound_attributes) + conn.to_sql(relation.arel) } end end @@ -586,12 +444,14 @@ module ActiveRecord # # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} - def where_values_hash(relation_table_name = table_name) + def where_values_hash(relation_table_name = klass.table_name) where_clause.to_h(relation_table_name) end - def scope_for_create - @scope_for_create ||= where_values_hash.merge(create_with_value) + def scope_for_create(attributes = nil) + scope = where_values_hash.merge!(create_with_value.stringify_keys) + scope.merge!(attributes) if attributes + scope end # Returns true if relation needs eager loading. @@ -643,6 +503,19 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end + def empty_scope? # :nodoc: + @values == klass.unscoped.values + end + + def has_limit_or_offset? # :nodoc: + limit_value || offset_value + end + + def alias_tracker(joins = [], aliases = nil) # :nodoc: + joins += [aliases] if aliases + ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) + end + protected def load_records(records) @@ -657,20 +530,44 @@ module ActiveRecord end def exec_queries(&block) - @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze - - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + skip_query_cache_if_necessary do + @records = + if eager_loading? + find_with_associations do |relation, join_dependency| + if ActiveRecord::NullRelation === relation + [] + else + rows = connection.select_all(relation.arel, "SQL") + join_dependency.instantiate(rows, &block) + end.freeze + end + else + 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 - @records.each(&:readonly!) if readonly_value + @records.each(&:readonly!) if readonly_value - @loaded = true - @records + @loaded = true + @records + end + end + + def skip_query_cache_if_necessary + if skip_query_cache_value + uncached do + yield + end + else + yield + end end def build_preloader diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index ee1f25ec84..561869017a 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,6 @@ -require_relative "batches/batch_enumerator" +# frozen_string_literal: true + +require "active_record/relation/batches/batch_enumerator" module ActiveRecord module Batches @@ -45,7 +47,12 @@ module ActiveRecord # handle from 10000 and beyond by setting the +:start+ and +:finish+ # option on each worker. # - # # Let's process from record 10_000 on. + # # In worker 1, let's process until 9999 records. + # Person.find_each(finish: 9_999) do |person| + # person.party_all_night! + # end + # + # # In worker 2, let's process from record 10_000 and onwards. # Person.find_each(start: 10_000) do |person| # person.party_all_night! # end @@ -209,6 +216,7 @@ module ActiveRecord relation = relation.reorder(batch_order).limit(batch_limit) relation = apply_limits(relation, start, finish) + relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching batch_relation = relation loop do @@ -243,20 +251,27 @@ module ActiveRecord end end - batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) + 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))) end end private def apply_limits(relation, start, finish) - relation = relation.where(arel_attribute(primary_key).gteq(start)) if start - relation = relation.where(arel_attribute(primary_key).lteq(finish)) if 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 end def batch_order - "#{quoted_table_name}.#{quoted_primary_key} ASC" + arel_attribute(primary_key).asc end def act_on_ignored_order(error_on_ignore) diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 3555779ec2..49697da3bf 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Batches class BatchEnumerator diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 8a54f8f2c3..cb0b06cfdc 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Calculations # Count the records. @@ -128,8 +130,8 @@ module ActiveRecord # end def calculate(operation, column_name) if has_include?(column_name) - relation = construct_relation_for_association_calculations - relation = relation.distinct if operation.to_s.downcase == "count" + relation = apply_join_dependency + relation.distinct! if operation.to_s.downcase == "count" relation.calculate(operation, column_name) else @@ -178,13 +180,15 @@ module ActiveRecord end if has_include?(column_names.first) - construct_relation_for_association_calculations.pluck(*column_names) + relation = apply_join_dependency + relation.pluck(*column_names) else + enforce_raw_sql_whitelist(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } - result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) } result.cast_values(klass.attribute_types) end end @@ -212,8 +216,13 @@ module ActiveRecord if operation == "count" column_name ||= select_for_count - column_name = primary_key if column_name == :all && distinct - distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i + if column_name == :all + if distinct && (group_values.any? || select_values.empty? && order_values.empty?) + column_name = primary_key + end + elsif column_name =~ /\s*DISTINCT[\s(]+/i + distinct = nil + end end if group_values.any? @@ -240,7 +249,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column_alias = column_name - if operation == "count" && (limit_value || offset_value) + if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?) # Shortcut when limit is zero. return 0 if limit_value == 0 @@ -252,6 +261,9 @@ module ActiveRecord column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) + if operation == "sum" && distinct + select_value.distinct = true + end column_alias = select_value.alias column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) @@ -260,7 +272,7 @@ module ActiveRecord query_builder = relation.arel end - result = @klass.connection.select_all(query_builder, nil, bound_attributes) + result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil) } row = result.first value = row && row.values.first type = result.column_types.fetch(column_alias) do @@ -311,7 +323,7 @@ module ActiveRecord relation.group_values = group_fields relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation.arel, nil, relation.bound_attributes) + calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) } if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -379,14 +391,16 @@ module ActiveRecord end def build_count_subquery(relation, column_name, distinct) - column_alias = Arel.sql("count_column") - subquery_alias = Arel.sql("subquery_for_count") + if column_name == :all + relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct + else + column_alias = Arel.sql("count_column") + relation.select_values = [ aggregate_column(column_name).as(column_alias) ] + end - aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) - relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + subquery = relation.arel.as(Arel.sql("subquery_for_count")) + select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false) - select_value = operation_over_aggregate_column(column_alias, "count", distinct) Arel::SelectManager.new(subquery).project(select_value) end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 48c4dcdef4..4863befec8 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Delegation # :nodoc: module DelegateCache # :nodoc: @@ -36,13 +38,12 @@ 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, :to_ary, :join, - :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + delegate :to_xml, :encode_with, :length, :each, :uniq, :join, + :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json, - :shuffle, :split, :index, to: :records + :shuffle, :split, :slice, :index, :rindex, to: :records - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, - :connection, :columns_hash, to: :klass + delegate :primary_key, :connection, to: :klass module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -73,13 +74,6 @@ module ActiveRecord end end end - - def delegate(method, opts = {}) - @delegation_mutex.synchronize do - return if method_defined?(method) - super - end - end end private @@ -91,7 +85,6 @@ module ActiveRecord elsif arel.respond_to?(method) ActiveSupport::Deprecation.warn \ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." - self.class.delegate method, to: :arel arel.public_send(method, *args, &block) else super diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index eee0f36f63..50d0f14b98 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/filters" module ActiveRecord @@ -16,9 +18,10 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order - # option if you want the results to be sorted. + # NOTE: The returned records are in the same order as the ids you provide. + # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where + # method and provide an explicit ActiveRecord::QueryMethods#order option. + # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound. # # ==== Find with lock # @@ -86,7 +89,7 @@ module ActiveRecord where(arg, *args).take! rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name) + @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -145,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! @@ -282,7 +285,7 @@ module ActiveRecord # * Hash - Finds the record that matches these +find+-style conditions # (such as <tt>{name: 'David'}</tt>). # * +false+ - Returns always +false+. - # * No args - Returns +false+ if the table is empty, +true+ otherwise. + # * No args - Returns +false+ if the relation is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -298,6 +301,7 @@ module ActiveRecord # Person.exists?(name: 'David') # Person.exists?(false) # Person.exists? + # Person.where(name: 'Spartacus', rating: 4).exists? def exists?(conditions = :none) if Base === conditions raise ArgumentError, <<-MSG.squish @@ -308,14 +312,14 @@ module ActiveRecord return false if !conditions || limit_value == 0 - relation = self unless eager_loading? - relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - - return false if ActiveRecord::NullRelation === relation + if eager_loading? + relation = apply_join_dependency(construct_join_dependency(eager_loading: false)) + return relation.exists?(conditions) + end - relation = construct_relation_for_exists(relation, conditions) + relation = construct_relation_for_exists(conditions) - connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) ? true : false + skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false rescue ::RangeError false end @@ -328,23 +332,23 @@ module ActiveRecord # of results obtained should be provided in the +result_size+ argument and # the expected number of results should be provided in the +expected_size+ # argument. - def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc: - conditions = arel.where_sql(@klass.arel_engine) + def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) # :nodoc: + conditions = arel.where_sql(@klass) conditions = " [#{conditions}]" if conditions name = @klass.name if ids.nil? error = "Couldn't find #{name}".dup error << " with#{conditions}" if conditions - raise RecordNotFound.new(error, name) + raise RecordNotFound.new(error, name, key) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) else error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup - error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - - raise RecordNotFound.new(error, name, primary_key, ids) + error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." + error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids + raise RecordNotFound.new(error, name, key, ids) end end @@ -364,26 +368,16 @@ module ActiveRecord # preexisting join in joins_values to categorizations (by way of # the `has_many :through` for categories). # - join_dependency = construct_join_dependency(joins_values) + join_dependency = construct_join_dependency - aliases = join_dependency.aliases - relation = select aliases.columns - relation = apply_join_dependency(relation, join_dependency) + relation = apply_join_dependency(join_dependency) + relation._select!(join_dependency.aliases.columns) - if block_given? - yield relation - else - if ActiveRecord::NullRelation === relation - [] - else - rows = connection.select_all(relation.arel, "SQL", relation.bound_attributes) - join_dependency.instantiate(rows, aliases) - end - end + yield relation, join_dependency end - def construct_relation_for_exists(relation, conditions) - relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + def construct_relation_for_exists(conditions) + relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) case conditions when Array, Hash @@ -395,22 +389,20 @@ module ActiveRecord relation end - def construct_join_dependency(joins = [], eager_loading: true) + def construct_join_dependency(eager_loading: true) including = eager_load_values + includes_values - ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) - end - - def construct_relation_for_association_calculations - apply_join_dependency(self, construct_join_dependency(joins_values)) + ActiveRecord::Associations::JoinDependency.new( + klass, table, including, alias_tracker(joins_values), eager_loading: eager_loading + ) end - def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency) + def apply_join_dependency(join_dependency = construct_join_dependency) + relation = except(:includes, :eager_load, :preload).joins!(join_dependency) if using_limitable_reflections?(join_dependency.reflections) relation else - if relation.limit_value + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end @@ -420,11 +412,13 @@ module ActiveRecord def limited_ids_for(relation) values = @klass.connection.columns_for_distinct( - "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) + connection.column_name_from_arel_node(arel_attribute(primary_key)), + relation.order_values + ) relation = relation.except(:select).select(values).distinct! - id_rows = @klass.connection.select_all(relation.arel, "SQL", relation.bound_attributes) + id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL") } id_rows.map { |row| row[primary_key] } end @@ -440,9 +434,12 @@ module ActiveRecord ids = ids.flatten.compact.uniq + model_name = @klass.name + case ids.size when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + error_message = "Couldn't find #{model_name} without an ID" + raise RecordNotFound.new(error_message, model_name, primary_key) when 1 result = find_one(ids.first) expects_array ? [ result ] : result @@ -450,7 +447,8 @@ module ActiveRecord find_some(ids) end rescue ::RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + error_message = "Couldn't find #{model_name} with an out of range ID" + raise RecordNotFound.new(error_message, model_name, primary_key, ids) end def find_one(id) @@ -534,7 +532,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 @@ -549,12 +551,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/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index 8945cb0cc5..c53a682aee 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class FromClause # :nodoc: @@ -8,14 +10,6 @@ module ActiveRecord @name = name end - def binds - if value.is_a?(Relation) - value.bound_attributes - else - [] - end - end - def merge(other) self end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 5dac00724a..b736b21525 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/keys" module ActiveRecord @@ -110,21 +112,20 @@ module ActiveRecord if other.klass == relation.klass relation.joins!(*other.joins_values) else - joins_dependency, rest = other.joins_values.partition do |join| + alias_tracker = nil + joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - true + alias_tracker ||= other.alias_tracker + ActiveRecord::Associations::JoinDependency.new( + other.klass, other.table, join, alias_tracker + ) else - false + join end end - join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, - joins_dependency, - []) - relation.joins! rest - - @relation = relation.joins join_dependency + relation.joins!(*joins_dependency) end end @@ -132,19 +133,17 @@ module ActiveRecord if other.reordering_value # override any order specified in the original relation relation.reorder! other.order_values - elsif other.order_values + elsif other.order_values.any? # merge in order_values from relation relation.order! other.order_values end - relation.extend(*other.extending_values) unless other.extending_values.blank? + extensions = other.extensions - relation.extensions + relation.extending!(*extensions) if extensions.any? end def merge_single_values - if relation.from_clause.empty? - relation.from_clause = other.from_clause - end - relation.lock_value ||= other.lock_value + relation.lock_value ||= other.lock_value if other.lock_value unless other.create_with_value.blank? relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) @@ -152,11 +151,15 @@ module ActiveRecord end def merge_clauses - CLAUSE_METHODS.each do |method| - clause = relation.get_value(method) - other_clause = other.get_value(method) - relation.set_value(method, clause.merge(other_clause)) + if relation.from_clause.empty? && !other.from_clause.empty? + relation.from_clause = other.from_clause end + + where_clause = relation.where_clause.merge(other.where_clause) + relation.where_clause = where_clause unless where_clause.empty? + + having_clause = relation.having_clause.merge(other.having_clause) + relation.having_clause = having_clause unless having_clause.empty? end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index eb80c9a00d..885c26d7aa 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder # :nodoc: delegate :resolve_column_aliases, to: :table @@ -6,12 +8,12 @@ module ActiveRecord @table = table @handlers = [] - register_handler(BasicObject, BasicObjectHandler.new) + register_handler(BasicObject, BasicObjectHandler.new(self)) register_handler(Base, BaseHandler.new(self)) - register_handler(Range, RangeHandler.new) - register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) + register_handler(Range, RangeHandler.new(self)) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) + register_handler(Set, ArrayHandler.new(self)) end def build_from_hash(attributes) @@ -19,11 +21,6 @@ module ActiveRecord expand_from_hash(attributes) end - def create_binds(attributes) - attributes = convert_dot_notation_to_hash(attributes) - create_binds_for_hash(attributes) - end - def self.references(attributes) attributes.map do |key, value| if value.is_a?(Hash) @@ -54,8 +51,11 @@ module ActiveRecord handler_for(value).call(attribute, value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. + def build_bind_attribute(column_name, value) + attr = Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + Arel::Nodes::BindParam.new(attr) + end + protected attr_reader :table @@ -66,29 +66,13 @@ module ActiveRecord attributes.flat_map do |key, value| if value.is_a?(Hash) && !table.has_column?(key) associated_predicate_builder(key).expand_from_hash(value) - else - build(table.arel_attribute(key), value) - end - end - end - - def create_binds_for_hash(attributes) - result = attributes.dup - binds = [] - - attributes.each do |column_name, value| - case - when value.is_a?(Hash) && !table.has_column?(column_name) - attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) - result[column_name] = attrs - binds += bvs - when table.associated_with?(column_name) + elsif table.associated_with?(key) # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - associated_table = table.associated_table(column_name) + associated_table = table.associated_table(key) if associated_table.polymorphic_association? case value.is_a?(Array) ? value.first : value when Base, Relation @@ -98,40 +82,18 @@ module ActiveRecord end klass ||= AssociationQueryValue - result[column_name] = klass.new(associated_table, value).queries.map do |query| - attrs, bvs = create_binds_for_hash(query) - binds.concat(bvs) - attrs - end - when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype) - first = value.begin - last = value.end - unless first.respond_to?(:infinite?) && first.infinite? - binds << build_bind_attribute(column_name, first) - first = Arel::Nodes::BindParam.new + queries = klass.new(associated_table, value).queries.map do |query| + expand_from_hash(query).reduce(&:and) end - unless last.respond_to?(:infinite?) && last.infinite? - binds << build_bind_attribute(column_name, last) - last = Arel::Nodes::BindParam.new - end - - result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) - when value.is_a?(Relation) - binds.concat(value.bound_attributes) + 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) else - if can_be_bound?(column_name, value) - bind_attribute = build_bind_attribute(column_name, value) - if value.is_a?(StatementCache::Substitute) || !bind_attribute.value_for_database.nil? - result[column_name] = Arel::Nodes::BindParam.new - binds << bind_attribute - else - result[column_name] = nil - end - end + build(table.arel_attribute(key), value) end end - - [result, binds] end private @@ -159,27 +121,14 @@ module ActiveRecord def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end - - def can_be_bound?(column_name, value) - case value - when Array, Range - table.type(column_name).respond_to?(:subtype) - else - !value.nil? && handler_for(value).is_a?(BasicObjectHandler) - end - end - - def build_bind_attribute(column_name, value) - Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) - end end end -require_relative "predicate_builder/array_handler" -require_relative "predicate_builder/base_handler" -require_relative "predicate_builder/basic_object_handler" -require_relative "predicate_builder/range_handler" -require_relative "predicate_builder/relation_handler" +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" -require_relative "predicate_builder/association_query_value" -require_relative "predicate_builder/polymorphic_array_value" +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" 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 1068e700e2..2fd75c8958 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -7,7 +9,6 @@ module ActiveRecord def call(attribute, value) return attribute.in([]) if value.empty? - return queries_predicates(value) if value.all? { |v| v.is_a?(Hash) } values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) @@ -17,7 +18,11 @@ module ActiveRecord case values.length when 0 then NullPredicate when 1 then predicate_builder.build(attribute, values.first) - else attribute.in(values) + else + bind_values = values.map do |v| + predicate_builder.build_bind_attribute(attribute.name, v) + end + attribute.in(bind_values) end unless nils.empty? @@ -29,8 +34,6 @@ module ActiveRecord array_predicates.inject(&:or) 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 :predicate_builder @@ -40,17 +43,6 @@ module ActiveRecord other end end - - private - def queries_predicates(queries) - if queries.size > 1 - queries.map do |query| - Arel::Nodes::And.new(predicate_builder.build_from_hash(query)) - end.inject(&:or) - else - predicate_builder.build_from_hash(queries.first) - end - end end end end 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 3e19646ae5..28c7483c95 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class AssociationQueryValue # :nodoc: @@ -7,7 +9,7 @@ module ActiveRecord end def queries - [associated_table.association_foreign_key.to_s => ids] + [associated_table.association_join_foreign_key.to_s => ids] end # TODO Change this to private once we've dropped Ruby 2.2 support. @@ -28,7 +30,7 @@ module ActiveRecord end def primary_key - associated_table.association_primary_key + associated_table.association_join_primary_key end def convert_to_id(value) 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 3bb1037885..112821135f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class BaseHandler # :nodoc: @@ -9,8 +11,6 @@ module ActiveRecord predicate_builder.build(attribute, value.id) 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 :predicate_builder 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 79cde00303..34db266f05 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 @@ -1,9 +1,20 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) - attribute.eq(value) + bind = predicate_builder.build_bind_attribute(attribute.name, value) + attribute.eq(bind) end + + protected + + attr_reader :predicate_builder end 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 7029ae5f47..e8e2f2c626 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class PolymorphicArrayValue # :nodoc: @@ -10,7 +12,7 @@ module ActiveRecord type_to_ids_mapping.map do |type, ids| { associated_table.association_foreign_type.to_s => type, - associated_table.association_foreign_key.to_s => ids.size > 1 ? ids : ids.first + associated_table.association_foreign_key.to_s => ids } end end @@ -27,7 +29,7 @@ module ActiveRecord end def primary_key(value) - associated_table.association_primary_key(base_class(value)) + associated_table.association_join_primary_key(base_class(value)) end def base_class(value) 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 5db778e19c..6d16579708 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,25 +1,41 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: - RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) + class RangeWithBinds < Struct.new(:begin, :end) + def exclude_end? + false + end + end + + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end 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? attribute.not_in([]) elsif value.exclude_end? - attribute.lt(value.end) + attribute.lt(end_bind) else - attribute.lteq(value.end) + attribute.lteq(end_bind) end elsif value.end.respond_to?(:infinite?) && value.end.infinite? - attribute.gteq(value.begin) + attribute.gteq(begin_bind) elsif value.exclude_end? - attribute.gteq(value.begin).and(attribute.lt(value.end)) + attribute.gteq(begin_bind).and(attribute.lt(end_bind)) else - attribute.between(value) + attribute.between(RangeWithBinds.new(begin_bind, end_bind)) end end + + protected + + attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 8a910a82fe..c8bbfa5051 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class RelationHandler # :nodoc: def call(attribute, value) + if value.eager_loading? + value = value.send(:apply_join_dependency) + end + if value.select_values.empty? value = value.select(value.arel_attribute(value.klass.primary_key)) end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index 0e1f64775d..3532f28858 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -1,8 +1,10 @@ -require_relative "../attribute" +# frozen_string_literal: true + +require "active_model/attribute" module ActiveRecord class Relation - class QueryAttribute < Attribute # :nodoc: + class QueryAttribute < ActiveModel::Attribute # :nodoc: def type_cast(value) value end @@ -14,6 +16,11 @@ module ActiveRecord def with_cast_value(value) QueryAttribute.new(name, value, type) end + + def nil? + !value_before_type_cast.is_a?(StatementCache::Substitute) && + (value_before_type_cast.nil? || value_for_database.nil?) + 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 9da8f96337..009624e194 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,7 +1,9 @@ -require_relative "from_clause" -require_relative "query_attribute" -require_relative "where_clause" -require_relative "where_clause_factory" +# frozen_string_literal: true + +require "active_record/relation/from_clause" +require "active_record/relation/query_attribute" +require "active_record/relation/where_clause" +require "active_record/relation/where_clause_factory" require "active_model/forbidden_attributes_protection" module ActiveRecord @@ -74,31 +76,6 @@ module ActiveRecord CODE end - def bound_attributes - if limit_value - limit_bind = Attribute.with_cast_value( - "LIMIT".freeze, - connection.sanitize_limit(limit_value), - Type.default_value, - ) - end - if offset_value - offset_bind = Attribute.with_cast_value( - "OFFSET".freeze, - offset_value.to_i, - Type.default_value, - ) - end - connection.combine_bind_parameters( - from_clause: from_clause.binds, - join_clause: arel.bind_values, - where_clause: where_clause.binds, - having_clause: having_clause.binds, - limit: limit_bind, - offset: offset_bind, - ) - end - alias extensions extending_values # Specify relationships to be included in the result set. For @@ -202,12 +179,13 @@ module ActiveRecord # Works in two unique ways. # - # First: takes a block so it can be used just like +Array#select+. + # First: takes a block so it can be used just like <tt>Array#select</tt>. # # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, - # converting them into an array and iterating through them using +Array#select+. + # converting them into an array and iterating through them using + # <tt>Array#select</tt>. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: @@ -317,6 +295,7 @@ module ActiveRecord spawn.order!(*args) end + # Same as #order but operates on relation in-place instead of copying. def order!(*args) # :nodoc: preprocess_order_args(args) @@ -338,6 +317,7 @@ module ActiveRecord spawn.reorder!(*args) end + # Same as #reorder but operates on relation in-place instead of copying. def reorder!(*args) # :nodoc: preprocess_order_args(args) @@ -398,7 +378,7 @@ module ActiveRecord 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 - set_value(scope, nil) + set_value(scope, DEFAULT_VALUES[scope]) when Hash scope.each do |key, target_value| if key != :where @@ -463,16 +443,14 @@ module ActiveRecord # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" # def left_outer_joins(*args) - check_if_method_has_arguments!(:left_outer_joins, args) - - args.compact! - args.flatten! - + check_if_method_has_arguments!(__callee__, args) spawn.left_outer_joins!(*args) end alias :left_joins :left_outer_joins def left_outer_joins!(*args) # :nodoc: + args.compact! + args.flatten! self.left_outer_joins_values += args self end @@ -657,6 +635,7 @@ module ActiveRecord self.where_clause = self.where_clause.or(other.where_clause) self.having_clause = having_clause.or(other.having_clause) + self.references_values += other.references_values self end @@ -797,7 +776,7 @@ module ActiveRecord value = sanitize_forbidden_attributes(value) self.create_with_value = create_with_value.merge(value) else - self.create_with_value = {} + self.create_with_value = FROZEN_EMPTY_HASH end self @@ -913,21 +892,27 @@ module ActiveRecord self end - # Returns the Arel object associated with the relation. - def arel # :nodoc: - @arel ||= build_arel + def skip_query_cache! # :nodoc: + self.skip_query_cache_value = true + self end - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values[name] || default_value_for(name) + # Returns the Arel object associated with the relation. + def arel(aliases = nil) # :nodoc: + @arel ||= build_arel(aliases) end - # Sets the relation value with the given name - def set_value(name, value) # :nodoc: - assert_mutability! - @values[name] = value - 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: + assert_mutability! + @values[name] = value + end private @@ -936,16 +921,30 @@ module ActiveRecord raise ImmutableRelation if defined?(@arel) && @arel end - def build_arel + def build_arel(aliases) arel = Arel::SelectManager.new(table) - build_joins(arel, joins_values.flatten) unless joins_values.empty? - build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty? + aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty? arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? - arel.take(Arel::Nodes::BindParam.new) if limit_value - arel.skip(Arel::Nodes::BindParam.new) if offset_value + if limit_value + limit_attribute = ActiveModel::Attribute.with_cast_value( + "LIMIT".freeze, + connection.sanitize_limit(limit_value), + Type.default_value, + ) + arel.take(Arel::Nodes::BindParam.new(limit_attribute)) + end + if offset_value + offset_attribute = ActiveModel::Attribute.with_cast_value( + "OFFSET".freeze, + offset_value.to_i, + Type.default_value, + ) + arel.skip(Arel::Nodes::BindParam.new(offset_attribute)) + end arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) @@ -964,6 +963,9 @@ module ActiveRecord name = from_clause.name case opts when Relation + if opts.eager_loading? + opts = opts.send(:apply_join_dependency) + end name ||= "subquery" opts.arel.as(name.to_s) else @@ -971,7 +973,7 @@ module ActiveRecord end end - def build_left_outer_joins(manager, outer_joins) + def build_left_outer_joins(manager, outer_joins, aliases) buckets = outer_joins.group_by do |join| case join when Hash, Symbol, Array @@ -981,10 +983,10 @@ module ActiveRecord end end - build_join_query(manager, buckets, Arel::Nodes::OuterJoin) + build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases) end - def build_joins(manager, joins) + def build_joins(manager, joins, aliases) buckets = joins.group_by do |join| case join when String @@ -1000,10 +1002,10 @@ module ActiveRecord end end - build_join_query(manager, buckets, Arel::Nodes::InnerJoin) + build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases) end - def build_join_query(manager, buckets, join_type) + def build_join_query(manager, buckets, join_type, aliases) buckets.default = [] association_joins = buckets[:association_join] @@ -1012,23 +1014,18 @@ module ActiveRecord string_joins = buckets[:string_join].map(&:strip).uniq join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + alias_tracker = alias_tracker(join_list, aliases) join_dependency = ActiveRecord::Associations::JoinDependency.new( - @klass, - association_joins, - join_list + klass, table, association_joins, alias_tracker ) - join_infos = join_dependency.join_constraints stashed_association_joins, join_type - - join_infos.each do |info| - info.joins.each { |join| manager.from(join) } - manager.bind_values.concat info.binds - end + joins = join_dependency.join_constraints(stashed_association_joins, join_type) + joins.each { |join| manager.from(join) } manager.join_sources.concat(join_list) - manager + alias_tracker.aliases end def convert_join_strings_to_ast(table, joins) @@ -1041,8 +1038,10 @@ module ActiveRecord def build_select(arel) if select_values.any? arel.project(*arel_columns(select_values.uniq)) + elsif klass.ignored_columns.any? + arel.project(*klass.column_names.map { |field| arel_attribute(field) }) else - arel.project(@klass.arel_table[Arel.star]) + arel.project(table[Arel.star]) end end @@ -1077,7 +1076,7 @@ module ActiveRecord end o.split(",").map! do |s| s.strip! - s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC") + s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC") end else o @@ -1086,6 +1085,10 @@ module ActiveRecord end def does_not_support_reverse?(order) + # Account for String subclasses like Arel::Nodes::SqlLiteral that + # override methods like #count. + order = String.new(order) unless order.instance_of?(String) + # Uses SQL function with multiple arguments. (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || # Uses "nulls first" like construction. @@ -1116,9 +1119,15 @@ module ActiveRecord def preprocess_order_args(order_args) order_args.map! do |arg| - klass.send(:sanitize_sql_for_order, arg) + klass.sanitize_sql_for_order(arg) end order_args.flatten! + + @klass.enforce_raw_sql_whitelist( + order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, + whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + ) + validate_order_args(order_args) references = order_args.grep(String) @@ -1167,7 +1176,7 @@ module ActiveRecord end end - STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope] + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) @@ -1179,23 +1188,15 @@ module ActiveRecord end alias having_clause_factory where_clause_factory - def default_value_for(name) - case name - when :create_with - FROZEN_EMPTY_HASH - when :readonly - false - when :where, :having - Relation::WhereClause.empty - when :from - Relation::FromClause.empty - when *Relation::MULTI_VALUE_METHODS - FROZEN_EMPTY_ARRAY - when *Relation::SINGLE_VALUE_METHODS - nil - else - raise ArgumentError, "unknown relation value #{name.inspect}" - end + DEFAULT_VALUES = { + create_with: FROZEN_EMPTY_HASH, + where: Relation::WhereClause.empty, + having: Relation::WhereClause.empty, + from: Relation::FromClause.empty + } + + Relation::MULTI_VALUE_METHODS.each do |value| + DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY end end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 31544c730e..a7d07d23e1 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation module RecordFetchWarning diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ddf7f825c1..617d8de8b2 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" -require_relative "merger" +require "active_record/relation/merger" module ActiveRecord module SpawnMethods diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 119910ee79..a502713e56 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -1,65 +1,63 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class WhereClause # :nodoc: - attr_reader :binds - delegate :any?, :empty?, to: :predicates - def initialize(predicates, binds) + def initialize(predicates) @predicates = predicates - @binds = binds end def +(other) WhereClause.new( predicates + other.predicates, - binds + other.binds, + ) + end + + def -(other) + WhereClause.new( + predicates - other.predicates, ) end def merge(other) WhereClause.new( predicates_unreferenced_by(other) + other.predicates, - non_conflicting_binds(other) + other.binds, ) end def except(*columns) - WhereClause.new(*except_predicates_and_binds(columns)) + WhereClause.new(except_predicates(columns)) end def or(other) - if empty? - self - elsif other.empty? - other + left = self - other + common = self - left + right = other - common + + if left.empty? || right.empty? + common else - WhereClause.new( - [ast.or(other.ast)], - binds + other.binds + or_clause = WhereClause.new( + [left.ast.or(right.ast)], ) + common + or_clause end end def to_h(table_name = nil) - equalities = predicates.grep(Arel::Nodes::Equality) + equalities = equalities(predicates) if table_name equalities = equalities.select do |node| node.left.relation.name == table_name end end - binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h - equalities.map { |node| - name = node.left.name - [name, binds.fetch(name.to_s) { - case node.right - when Array then node.right.map(&:val) - when Arel::Nodes::Casted, Arel::Nodes::Quoted - node.right.val - end - }] + name = node.left.name.to_s + value = extract_node_value(node.right) + [name, value] }.to_h end @@ -69,20 +67,17 @@ module ActiveRecord def ==(other) other.is_a?(WhereClause) && - predicates == other.predicates && - binds == other.binds + predicates == other.predicates end def invert - WhereClause.new(inverted_predicates, binds) + WhereClause.new(inverted_predicates) end def self.empty - @empty ||= new([], []) + @empty ||= new([]) 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 :predicates @@ -95,6 +90,20 @@ module ActiveRecord end private + def equalities(predicates) + equalities = [] + + predicates.each do |node| + case node + when Arel::Nodes::Equality + equalities << node + when Arel::Nodes::And + equalities.concat equalities(node.children) + end + end + + equalities + end def predicates_unreferenced_by(other) predicates.reject do |n| @@ -106,12 +115,6 @@ module ActiveRecord node.respond_to?(:operator) && node.operator == :== end - def non_conflicting_binds(other) - conflicts = referenced_columns & other.referenced_columns - conflicts.map! { |node| node.name.to_s } - binds.reject { |attr| conflicts.include?(attr.name) } - end - def inverted_predicates predicates.map { |node| invert_predicate(node) } end @@ -131,43 +134,22 @@ module ActiveRecord end end - def except_predicates_and_binds(columns) - except_binds = [] - binds_index = 0 - - predicates = self.predicates.reject do |node| - except = \ - case node - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual - binds_contains = node.grep(Arel::Nodes::BindParam).size - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) - end - - if except && binds_contains > 0 - (binds_index...(binds_index + binds_contains)).each do |i| - except_binds[i] = true - end + def except_predicates(columns) + predicates.reject do |node| + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) end - - binds_index += binds_contains if binds_contains - - except end - - binds = self.binds.reject.with_index do |_, i| - except_binds[i] - end - - [predicates, binds] end def predicates_with_wrapped_sql_literals non_empty_predicates.map do |node| - if Arel::Nodes::Equality === node - node - else + case node + when Arel::Nodes::SqlLiteral, ::String wrap_sql_literal(node) + else node end end end @@ -183,6 +165,22 @@ module ActiveRecord end Arel::Nodes::Grouping.new(node) end + + def extract_node_value(node) + case node + when Array + node.map { |v| extract_node_value(v) } + when Arel::Nodes::Casted, Arel::Nodes::Quoted + node.val + when Arel::Nodes::BindParam + value = node.value + if value.respond_to?(:value_before_type_cast) + value.value_before_type_cast + else + value + end + end + end end end end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index b862dd56a5..4ae94f4bfe 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class WhereClauseFactory # :nodoc: @@ -9,69 +11,25 @@ module ActiveRecord def build(opts, other) case opts when String, Array - parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + 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! - if perform_case_sensitive?(options = other.last) - parts, binds = build_for_case_sensitive(attributes, options) - else - attributes, binds = predicate_builder.create_binds(attributes) - parts = predicate_builder.build_from_hash(attributes) - end + parts = predicate_builder.build_from_hash(attributes) when Arel::Nodes::Node parts = [opts] else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end - WhereClause.new(parts, binds || []) + WhereClause.new(parts) 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 :klass, :predicate_builder - - private - - def perform_case_sensitive?(options) - options && options.key?(:case_sensitive) - end - - def build_for_case_sensitive(attributes, options) - parts, binds = [], [] - table = klass.arel_table - - attributes.each do |attribute, value| - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key.to_s - value = value[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - parts << table[attribute].eq(value) - else - column = klass.column_for_attribute(attribute) - - binds << predicate_builder.send(:build_bind_attribute, attribute, value) - value = Arel::Nodes::BindParam.new - - predicate = if options[:case_sensitive] - klass.connection.case_sensitive_comparison(table, attribute, column, value) - else - klass.connection.case_insensitive_comparison(table, attribute, column, value) - end - - parts << predicate - end - end - - [parts, binds] - end end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 26b1d48e9e..e54e8086dd 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord ### # This class encapsulates a result returned from calling diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index b79eb2263f..4975cb8967 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/per_thread_registry" module ActiveRecord diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 64bda1539c..58da106092 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,74 +1,139 @@ +# frozen_string_literal: true + module ActiveRecord module Sanitization extend ActiveSupport::Concern module ClassMethods - private + # Accepts an array or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) + return nil if condition.blank? - # Accepts an array or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # - # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition) # :doc: - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - else condition - end + case condition + when Array; sanitize_sql_array(condition) + else condition end - alias :sanitize_sql :sanitize_sql_for_conditions - alias :sanitize_conditions :sanitize_sql - deprecate sanitize_conditions: :sanitize_sql + end + alias :sanitize_sql :sanitize_sql_for_conditions - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # - # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) - # # => "name=NULL and group_id=4" - # - # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) - # # => "name=NULL and group_id=4" - # - # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) - # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" - # - # sanitize_sql_for_assignment("name=NULL and group_id='4'") - # # => "name=NULL and group_id='4'" - def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc: - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) - else assignments - end + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # + # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" + def sanitize_sql_for_assignment(assignments, default_table_name = table_name) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) + else assignments end + end - # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for an ORDER clause. - # - # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) - # # => "field(id, 1,3,2)" - # - # sanitize_sql_for_order("id ASC") - # # => "id ASC" - def sanitize_sql_for_order(condition) # :doc: - if condition.is_a?(Array) && condition.first.to_s.include?("?") - sanitize_sql_array(condition) - else - condition + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for an ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) + if condition.is_a?(Array) && condition.first.to_s.include?("?") + enforce_raw_sql_whitelist([condition.first], + whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + ) + + # Ensure we aren't dealing with a subclass of String that might + # override methods we use (eg. Arel::Nodes::SqlLiteral). + if condition.first.kind_of?(String) && !condition.first.instance_of?(String) + condition = [String.new(condition.first), *condition[1..-1]] end + + Arel.sql(sanitize_sql_array(condition)) + else + condition + end + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" + def sanitize_sql_hash_for_assignment(attrs, table) + c = connection + attrs.map do |attr, value| + type = type_for_attribute(attr.to_s) + value = type.serialize(type.cast(value)) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" + end.join(", ") + end + + # Sanitizes a +string+ so that it is safe to use within an SQL + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_array(ary) + statement, *values = ary + if values.first.is_a?(Hash) && /:\w+/.match?(statement) + replace_named_bind_variables(statement, values.first) + elsif statement.include?("?") + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } end + end + private # Accepts a hash of SQL conditions and replaces those attributes # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] # relationship with their expanded aggregate attribute values. @@ -103,61 +168,6 @@ module ActiveRecord expanded_attrs end - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # - # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") - # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" - def sanitize_sql_hash_for_assignment(attrs, table) # :doc: - c = connection - attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).serialize(value) - "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" - end.join(", ") - end - - # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". - # - # sanitize_sql_like("100%") - # # => "100\\%" - # - # sanitize_sql_like("snake_cased_string") - # # => "snake\\_cased\\_string" - # - # sanitize_sql_like("100%", "!") - # # => "100!%" - # - # sanitize_sql_like("snake_cased_string", "!") - # # => "snake!_cased!_string" - def sanitize_sql_like(string, escape_character = "\\") # :doc: - pattern = Regexp.union(escape_character, "%", "_") - string.gsub(pattern) { |x| [escape_character, x].join } - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # - # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) # :doc: - statement, *values = ary - if values.first.is_a?(Hash) && /:\w+/.match?(statement) - replace_named_bind_variables(statement, values.first) - elsif statement.include?("?") - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end - def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) bound = values.dup @@ -205,10 +215,5 @@ module ActiveRecord end end end - - def quoted_id # :nodoc: - self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) - end - deprecate :quoted_id end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 5104427339..1e121f2a09 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record \Schema # @@ -37,7 +39,7 @@ module ActiveRecord # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): # - # ActiveRecord::Schema.define(version: 20380119000001) do + # ActiveRecord::Schema.define(version: 2038_01_19_000001) do # ... # end def self.define(info = {}, &block) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 66a2846f3a..16ccba6b6c 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "stringio" module ActiveRecord @@ -17,7 +19,7 @@ module ActiveRecord class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) - new(connection, generate_options(config)).dump(stream) + connection.create_schema_dumper(generate_options(config)).dump(stream) stream end @@ -80,16 +82,8 @@ HEADER stream.puts "end" end + # extensions are only supported by PostgreSQL def extensions(stream) - return unless @connection.supports_extensions? - extensions = @connection.extensions - if extensions.any? - stream.puts " # These are extensions that must be enabled in order to support this database" - extensions.each do |extension| - stream.puts " enable_extension #{extension.inspect}" - end - stream.puts - end end def tables(stream) @@ -121,7 +115,7 @@ HEADER when String tbl.print ", primary_key: #{pk.inspect}" unless pk == "id" pkcol = columns.detect { |c| c.name == pk } - pkcolspec = @connection.column_spec_for_primary_key(pkcol) + pkcolspec = column_spec_for_primary_key(pkcol) if pkcolspec.present? tbl.print ", #{format_colspec(pkcolspec)}" end @@ -130,20 +124,19 @@ HEADER else tbl.print ", id: false" end - tbl.print ", force: :cascade" table_options = @connection.table_options(table) if table_options.present? tbl.print ", #{format_options(table_options)}" end - tbl.puts " do |t|" + tbl.puts ", force: :cascade do |t|" # then dump all non-primary key columns columns.each do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - type, colspec = @connection.column_spec(column) + type, colspec = column_spec(column) tbl.print " t.#{type} #{column.name.inspect}" tbl.print ", #{format_colspec(colspec)}" if colspec.present? tbl.puts @@ -161,8 +154,6 @@ HEADER stream.puts "# #{e.message}" stream.puts end - - stream end # Keep it for indexing materialized views @@ -193,8 +184,9 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? - index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? + index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present? + index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present? + index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present? index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type @@ -240,8 +232,18 @@ HEADER options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") end + def format_index_parts(options) + if options.is_a?(Hash) + "{ #{format_options(options)} }" + else + options.inspect + end + end + def remove_prefix_and_suffix(table) - table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") + prefix = Regexp.escape(@options[:table_name_prefix].to_s) + suffix = Regexp.escape(@options[:table_name_suffix].to_s) + table.sub(/\A#{prefix}(.+)#{suffix}\z/, "\\1") end def ignored?(table_name) diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 6dbabd69a1..f2d8b038fa 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,5 +1,7 @@ -require_relative "scoping/default" -require_relative "scoping/named" +# frozen_string_literal: true + +require "active_record/scoping/default" +require "active_record/scoping/named" module ActiveRecord # This class is used to create a table that keeps track of which migrations diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 94e0ef6724..01ac56570a 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/per_thread_registry" module ActiveRecord @@ -9,23 +11,23 @@ module ActiveRecord include Named end - module ClassMethods - def current_scope(skip_inherited_scope = false) # :nodoc: + module ClassMethods # :nodoc: + def current_scope(skip_inherited_scope = false) ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) end - def current_scope=(scope) #:nodoc: + def current_scope=(scope) ScopeRegistry.set_value_for(:current_scope, self, scope) end # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: + def scope_attributes all.scope_for_create end # Are there attributes associated with this scope? - def scope_attributes? # :nodoc: + def scope_attributes? current_scope end end @@ -33,9 +35,8 @@ module ActiveRecord def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? - self.class.scope_attributes.each do |att, value| - send("#{att}=", value) if respond_to?("#{att}=") - end + attributes = self.class.scope_attributes + _assign_attributes(attributes) if attributes.any? end def initialize_internals_callback # :nodoc: diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 70b2693b28..8c612df27a 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Scoping module Default @@ -109,7 +111,7 @@ module ActiveRecord # The user has defined their own default scope method, so call that evaluate_default_scope do if scope = default_scope - (base_rel ||= relation).merge(scope) + (base_rel ||= relation).merge!(scope) end end elsif default_scopes.any? @@ -117,7 +119,7 @@ module ActiveRecord evaluate_default_scope do default_scopes.inject(base_rel) do |default_scope, scope| scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) - default_scope.merge(base_rel.instance_exec(&scope)) + default_scope.merge!(base_rel.instance_exec(&scope)) end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index b4026fabb2..752655aa05 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array" require "active_support/core_ext/hash/except" require "active_support/core_ext/kernel/singleton_class" @@ -22,13 +24,29 @@ module ActiveRecord # You can define a scope that applies to all finders using # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all + current_scope = self.current_scope + if current_scope - current_scope.clone + if self == current_scope.klass + current_scope.clone + else + relation.merge!(current_scope) + end else default_scoped end end + def scope_for_association(scope = relation) # :nodoc: + current_scope = self.current_scope + + if current_scope && current_scope.empty_scope? + scope + else + default_scoped(scope) + end + end + def default_scoped(scope = relation) # :nodoc: build_default_scope(scope) || scope end @@ -153,6 +171,12 @@ module ActiveRecord "a class method with the same name." end + if method_defined_within?(name, Relation) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ + "an instance method with the same name." + end + valid_scope_name?(name) extension = Module.new(&block) if block diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 115799cc20..bcdb33901b 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module SecureToken extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index db2bd0b55e..741fea43ce 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord #:nodoc: # = Active Record \Serialization module Serialization diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 1877489e55..59acd63a0f 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # Statement cache is used to cache a single statement in order to avoid creating the AST again. # Initializing the cache is done by passing the statement in the create block: @@ -9,7 +11,7 @@ module ActiveRecord # The cached statement is executed by using the # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method: # - # cache.execute([], Book, Book.connection) + # cache.execute([], Book.connection) # # The relation returned by the block is cached, and for each # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] @@ -24,7 +26,7 @@ module ActiveRecord # # And pass the bind values as the first argument of +execute+ call. # - # cache.execute(["my book"], Book, Book.connection) + # cache.execute(["my book"], Book.connection) class StatementCache # :nodoc: class Substitute; end # :nodoc: @@ -85,27 +87,35 @@ module ActiveRecord end end - attr_reader :bind_map, :query_builder - def self.create(connection, block = Proc.new) - relation = block.call Params.new - bind_map = BindMap.new relation.bound_attributes - query_builder = connection.cacheable_query(self, relation.arel) - new query_builder, bind_map + relation = block.call Params.new + query_builder, binds = connection.cacheable_query(self, relation.arel) + bind_map = BindMap.new(binds) + new(query_builder, bind_map, relation.klass) end - def initialize(query_builder, bind_map) + def initialize(query_builder, bind_map, klass) @query_builder = query_builder - @bind_map = bind_map + @bind_map = bind_map + @klass = klass end - def execute(params, klass, connection, &block) + def execute(params, connection, &block) bind_values = bind_map.bind params sql = query_builder.sql_for bind_values, connection klass.find_by_sql(sql, bind_values, preparable: true, &block) end - alias :call :execute + + def self.unsupported_value?(value) + case value + when NilClass, Array, Range, Hash, Relation, Base then true + end + end + + protected + + 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 006afe7495..202b82fa61 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/indifferent_access" module ActiveRecord diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index d9acb1a1dc..8cdb8e0765 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # ActiveRecord::Suppressor prevents the receiver from being saved during # a given block. diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 71efc1829a..0459cbdc59 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord class TableMetadata # :nodoc: - delegate :foreign_type, :foreign_key, to: :association, prefix: true - delegate :association_primary_key, to: :association + delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true def initialize(klass, arel_table, association = nil) @klass = klass diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index ba686fc562..4657e51e6d 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -162,13 +164,12 @@ module ActiveRecord end def migrate - raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + check_target_version verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true - version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, version) do |migration| + Migrator.migrate(migrations_paths, target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! @@ -176,6 +177,16 @@ module ActiveRecord Migration.verbose = verbose_was end + def check_target_version + if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"])) + raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`" + end + end + + def target_version + ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty? + end + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -223,22 +234,22 @@ 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) # :nodoc: + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc: file ||= schema_file(format) + check_schema_file(file) + ActiveRecord::Base.establish_connection(configuration) + case format when :ruby - check_schema_file(file) - ActiveRecord::Base.establish_connection(configuration) load(file) when :sql - check_schema_file(file) structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = environment end def schema_file(format = ActiveRecord::Base.schema_format) @@ -251,16 +262,16 @@ module ActiveRecord end def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration| - load_schema configuration, format, file + each_current_configuration(environment) { |configuration, configuration_environment| + load_schema configuration, format, file, configuration_environment } ActiveRecord::Base.establish_connection(environment.to_sym) end def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} - message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) + message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}.dup + message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails.root) Kernel.abort message end end @@ -299,9 +310,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - configurations = ActiveRecord::Base.configurations.values_at(*environments) - configurations.compact.each do |configuration| - yield configuration unless configuration["database"].blank? + ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| + next unless configuration["database"] + + yield configuration, configuration_environment 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 ff6745f7b5..e697fa6def 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: - ACCESS_DENIED_ERROR = 1045 - delegate :connection, :establish_connection, to: ActiveRecord::Base def initialize(configuration) @@ -19,20 +19,6 @@ module ActiveRecord else raise end - rescue error_class => error - if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR - $stdout.print error.message - establish_connection root_configuration_without_database - connection.create_database configuration["database"], creation_options - if configuration["username"] != "root" - connection.execute grant_statement.gsub(/\s+/, " ").strip - end - establish_connection configuration - else - $stderr.puts error.inspect - $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" - $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration["encoding"] - end end def drop @@ -97,37 +83,6 @@ module ActiveRecord end end - def error_class - if configuration["adapter"].include?("jdbc") - require_relative "../railties/jdbcmysql_error" - ArJdbcMySQL::Error - elsif defined?(Mysql2) - Mysql2::Error - else - StandardError - end - end - - def grant_statement - <<-SQL -GRANT ALL PRIVILEGES ON `#{configuration['database']}`.* - TO '#{configuration['username']}'@'localhost' -IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; - SQL - end - - def root_configuration_without_database - configuration_without_database.merge( - "username" => "root", - "password" => root_password - ) - end - - def root_password - $stdout.print "Please provide the root password for your MySQL installation\n>" - $stdin.gets.strip - end - def prepare_command_options args = { "host" => "--host", diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 7f1a768d8b..647e066137 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "tempfile" module ActiveRecord @@ -20,7 +22,7 @@ module ActiveRecord configuration.merge("encoding" => encoding) establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database .* already exists/.match?(error.message) + if error.cause.is_a?(PG::DuplicateDatabase) raise DatabaseAlreadyExists else raise @@ -115,7 +117,7 @@ module ActiveRecord end def run_cmd_error(cmd, args, action) - msg = "failed to execute:\n" + msg = "failed to execute:\n".dup msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg @@ -134,7 +136,7 @@ module ActiveRecord ensure tempfile.close end - FileUtils.mv(tempfile.path, filename) + FileUtils.cp(tempfile.path, filename) end end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 01562b21e9..dfe599c4dd 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Tasks # :nodoc: class SQLiteDatabaseTasks # :nodoc: @@ -71,7 +73,7 @@ module ActiveRecord end def run_cmd_error(cmd, args) - msg = "failed to execute:\n" + msg = "failed to execute:\n".dup msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index dc4540eea6..5da3759e5a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module ActiveRecord # = Active Record \Timestamp # @@ -86,7 +87,7 @@ module ActiveRecord all_timestamp_attributes_in_model.each do |column| if !attribute_present?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end end @@ -100,7 +101,7 @@ module ActiveRecord timestamp_attributes_for_update_in_model.each do |column| next if will_save_change_to_attribute?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end super(*args) diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index cacde9c881..f70b7c50a2 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record Touch Later module TouchLater diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 463bb1f314..97cba5d1c7 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions @@ -168,7 +170,7 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html + # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # # === \Callbacks @@ -188,7 +190,7 @@ module ActiveRecord # # === Caveats # - # If you're on MySQL, then do not use Data Definition Language(DDL) operations in nested + # If you're on MySQL, then do not use Data Definition Language (DDL) operations in nested # transactions blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically # releases all savepoints upon executing a DDL operation. When +transaction+ @@ -283,7 +285,7 @@ module ActiveRecord fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) - options[:if].unshift("transaction_include_any_action?(#{fire_on})") + options[:if].unshift(-> { transaction_include_any_action?(fire_on) }) end end @@ -430,8 +432,8 @@ module ActiveRecord @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] pk = self.class.primary_key - if pk && read_attribute(pk) != restore_state[:id] - write_attribute(pk, restore_state[:id]) + if pk && _read_attribute(pk) != restore_state[:id] + _write_attribute(pk, restore_state[:id]) end freeze if restore_state[:frozen?] end @@ -470,7 +472,7 @@ module ActiveRecord # if it's associated with a transaction, then the state of the Active Record # object will be updated to reflect the current state of the transaction. # - # The +@transaction_state+ variable stores the states of the associated + # The <tt>@transaction_state</tt> variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required). # Each Active Record object inside of a transaction carries that transaction's diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb index ddcb5f2a7a..3cf70eafb8 100644 --- a/activerecord/lib/active_record/translation.rb +++ b/activerecord/lib/active_record/translation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Translation include ActiveModel::Translation diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 6f4e35b159..c303186ef2 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + require "active_model/type" -require_relative "type/internal/timezone" +require "active_record/type/internal/timezone" -require_relative "type/date" -require_relative "type/date_time" -require_relative "type/decimal_without_scale" -require_relative "type/json" -require_relative "type/time" -require_relative "type/text" -require_relative "type/unsigned_integer" +require "active_record/type/date" +require "active_record/type/date_time" +require "active_record/type/decimal_without_scale" +require "active_record/type/json" +require "active_record/type/time" +require "active_record/type/text" +require "active_record/type/unsigned_integer" -require_relative "type/serialized" -require_relative "type/adapter_specific_registry" +require "active_record/type/serialized" +require "active_record/type/adapter_specific_registry" -require_relative "type/type_map" -require_relative "type/hash_lookup_type_map" +require "active_record/type/type_map" +require "active_record/type/hash_lookup_type_map" module ActiveRecord module Type diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index 7cc866f7a7..e7468aa542 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/type/registry" module ActiveRecord diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index ccafed054e..8177074a20 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class Date < ActiveModel::Type::Date diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 1fb9380ecd..4acde6b9f8 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class DateTime < ActiveModel::Type::DateTime diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb index 53a5e205da..a207940dc7 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 0145d5d6c1..db9853fbcc 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb index 947e06158a..3059755752 100644 --- a/activerecord/lib/active_record/type/internal/timezone.rb +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type module Internal diff --git a/activerecord/lib/active_record/type/json.rb b/activerecord/lib/active_record/type/json.rb index c4732fe388..3f9ff22796 100644 --- a/activerecord/lib/active_record/type/json.rb +++ b/activerecord/lib/active_record/type/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class Json < ActiveModel::Type::Value diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index edbd20a6c1..e882784691 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb index cb1949700a..6d19696671 100644 --- a/activerecord/lib/active_record/type/text.rb +++ b/activerecord/lib/active_record/type/text.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class Text < ActiveModel::Type::String # :nodoc: diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index b9bac87c67..f4da1ecf2c 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class Time < ActiveModel::Type::Time diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 7bce82a1ff..fc40b460f0 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "concurrent/map" module ActiveRecord diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb index 9ae0109f9f..4619528f81 100644 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Type class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 8b12a30c6a..2e5f45fa3d 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -1,5 +1,7 @@ -require_relative "type_caster/map" -require_relative "type_caster/connection" +# frozen_string_literal: true + +require "active_record/type_caster/map" +require "active_record/type_caster/connection" module ActiveRecord module TypeCaster # :nodoc: diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 9f7bbe8843..af4e4e37e2 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module TypeCaster class Connection # :nodoc: diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 9f79723125..d51350ba83 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module TypeCaster class Map # :nodoc: diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 046b69bee2..ca27a3f0ab 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord # = Active Record \RecordInvalid # @@ -84,8 +86,8 @@ module ActiveRecord end end -require_relative "validations/associated" -require_relative "validations/uniqueness" -require_relative "validations/presence" -require_relative "validations/absence" -require_relative "validations/length" +require "active_record/validations/associated" +require "active_record/validations/uniqueness" +require "active_record/validations/presence" +require "active_record/validations/absence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb index 641d041f3d..6afb9eabd2 100644 --- a/activerecord/lib/active_record/validations/absence.rb +++ b/activerecord/lib/active_record/validations/absence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Validations class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index c695965d7b..3538aeec22 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 0e0cebce4a..f47b14ae3a 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 7cfd55f516..75e97e1997 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 154cf5f1a4..4c2c5dd852 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator # :nodoc: @@ -6,6 +8,10 @@ module ActiveRecord raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \ "Pass a callable instead: `conditions: -> { where(approved: true) }`" end + unless Array(options[:scope]).all? { |scope| scope.respond_to?(:to_sym) } + raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \ + "Pass a symbol or an array of symbols instead: `scope: :user_id`" + end super({ case_sensitive: true }.merge!(options)) @klass = options[:class] end @@ -50,7 +56,33 @@ module ActiveRecord end def build_relation(klass, attribute, value) - klass.unscoped.where!({ attribute => value }, options) + if reflection = klass._reflect_on_association(attribute) + attribute = reflection.foreign_key + value = value.attributes[reflection.klass.primary_key] unless value.nil? + end + + if value.nil? + return klass.unscoped.where!(attribute => value) + end + + # the attribute may be an aliased attribute + if klass.attribute_alias?(attribute) + attribute = klass.attribute_alias(attribute) + end + + attribute_name = attribute.to_s + value = klass.predicate_builder.build_bind_attribute(attribute_name, value) + + table = klass.arel_table + column = klass.columns_hash[attribute_name] + + comparison = if !options[:case_sensitive] + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + klass.connection.case_insensitive_comparison(table, attribute, column, value) + else + klass.connection.case_sensitive_comparison(table, attribute, column, value) + end + klass.unscoped.where!(comparison) end def scope_relation(record, relation) @@ -173,9 +205,7 @@ module ActiveRecord # | # Boom! We now have a duplicate # | # title! # - # This could even happen if you use transactions with the 'serializable' - # isolation level. The best way to work around this problem is to add a unique - # index to the database table using + # The best way to work around this problem is to add a unique index to the database table using # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. # In the rare case that a race condition occurs, the database will guarantee # the field's uniqueness. @@ -187,7 +217,7 @@ module ActiveRecord # can catch it and restart the transaction (e.g. by telling the user # that the title already exists, and asking them to re-enter the title). # This technique is also known as - # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control]. + # {optimistic concurrency control}[https://en.wikipedia.org/wiki/Optimistic_concurrency_control]. # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 146cfacc18..6b0d82d8fc 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "gem_version" module ActiveRecord diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index a79b8eafea..a7e5e373a7 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/generators/named_base" require "rails/generators/active_model" require "rails/generators/active_record/migration" diff --git a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb new file mode 100644 index 0000000000..35d5664400 --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "rails/generators/active_record" + +module ActiveRecord + module Generators # :nodoc: + class ApplicationRecordGenerator < ::Rails::Generators::Base # :nodoc: + source_root File.expand_path("templates", __dir__) + + # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. + def create_application_record + template "application_record.rb", application_record_file_name + end + + private + + def application_record_file_name + @application_record_file_name ||= + if namespaced? + "app/models/#{namespaced_path}/application_record.rb" + else + "app/models/application_record.rb" + end + end + end + end +end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt index 60050e0bf8..60050e0bf8 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb +++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 47c0981a49..4ceb502c5d 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/generators/migration" module ActiveRecord 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 1f1c47499b..856fcc5897 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/generators/active_record" module ActiveRecord @@ -26,7 +28,7 @@ module ActiveRecord def set_local_assigns! @migration_template = "migration.rb" case file_name - when /^(add|remove)_.*_(?:to|from)_(.*)/ + when /^(add)_.*_to_(.*)/, /^(remove)_.*?_from_(.*)/ @migration_action = $1 @table_name = normalize_table_name($2) when /join_table/ diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt index 5f7201cfe1..5f7201cfe1 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt index 481c70201b..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 5cec07d2e3..25e54f3ac8 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/generators/active_record" module ActiveRecord @@ -21,13 +23,11 @@ module ActiveRecord end def create_model_file - generate_application_record template "model.rb", File.join("app/models", class_path, "#{file_name}.rb") end def create_module_file return if regular_class_path.empty? - generate_application_record template "module.rb", File.join("app/models", "#{class_path.join('/')}.rb") if behavior == :invoke end @@ -39,31 +39,10 @@ module ActiveRecord attributes.select { |a| !a.reference? && a.has_index? } end - # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. - def generate_application_record - if behavior == :invoke && !application_record_exist? - template "application_record.rb", application_record_file_name - end - end - # Used by the migration template to determine the parent name of the model def parent_class_name options[:parent] || "ApplicationRecord" end - - def application_record_exist? - file_exist = nil - in_root { file_exist = File.exist?(application_record_file_name) } - file_exist - end - - def application_record_file_name - @application_record_file_name ||= if mountable_engine? - "app/models/#{namespaced_path}/application_record.rb" - else - "app/models/application_record.rb" - end - end end end end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt index 55dc65c8ad..55dc65c8ad 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt index a3bf1c37b6..a3bf1c37b6 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index b0d8050721..f977b2997b 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionHandling def fake_connection(config) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index a1fb6427f9..9aaa2852d0 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/book" require "models/post" @@ -156,26 +158,6 @@ module ActiveRecord end end - # test resetting sequences in odd tables in PostgreSQL - if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) - require "models/movie" - require "models/subscriber" - - def test_reset_empty_table_with_custom_pk - Movie.delete_all - Movie.connection.reset_pk_sequence! "movies" - assert_equal 1, Movie.create(name: "fight club").id - end - - def test_reset_table_with_non_integer_pk - Subscriber.delete_all - Subscriber.connection.reset_pk_sequence! "subscribers" - sub = Subscriber.new(name: "robert drake") - sub.id = "bob drake" - assert_nothing_raised { sub.save! } - end - end - def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" error = assert_raises(ActiveRecord::RecordNotUnique) do @@ -211,6 +193,28 @@ module ActiveRecord end end + def test_exceptions_from_notifications_are_not_translated + original_error = StandardError.new("This StandardError shouldn't get translated") + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error } + actual_error = assert_raises(StandardError) do + @connection.execute("SELECT * FROM posts") + end + + assert_equal original_error, actual_error + + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_database_related_exceptions_are_translated_to_statement_invalid + error = assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute("This is a syntax error") + end + + assert_instance_of ActiveRecord::StatementInvalid, error + assert_kind_of Exception, error.cause + end + def test_select_all_always_return_activerecord_result result = @connection.select_all "SELECT * FROM posts" assert result.is_a?(ActiveRecord::Result) @@ -220,16 +224,47 @@ module ActiveRecord def test_select_all_with_legacy_binds post = Post.create!(title: "foo", body: "bar") expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") - result = @connection.select_all("SELECT * FROM posts WHERE id = #{bind_param.to_sql}", nil, [[nil, post.id]]) + result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new(nil).to_sql}", nil, [[nil, post.id]]) assert_equal expected.to_hash, result.to_hash end + + def test_insert_update_delete_with_legacy_binds + binds = [[nil, 1]] + bind_param = Arel::Nodes::BindParam.new(nil) + + id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds) + assert_equal 1, id + + @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds) + result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + assert_equal({ "id" => 1, "title" => "foo" }, result.first) + + @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + assert_nil result.first + end + + def test_insert_update_delete_with_binds + binds = [Relation::QueryAttribute.new("id", 1, Type.default_value)] + bind_param = Arel::Nodes::BindParam.new(nil) + + id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds) + assert_equal 1, id + + @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds) + result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + assert_equal({ "id" => 1, "title" => "foo" }, result.first) + + @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds) + assert_nil result.first + end end def test_select_methods_passing_a_association_relation author = Author.create!(name: "john") Post.create!(author: author, title: "foo", body: "bar") query = author.posts.where(title: "foo").select(:title) - assert_equal({ "title" => "foo" }, @connection.select_one(query.arel, nil, query.bound_attributes)) assert_equal({ "title" => "foo" }, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) assert_equal "foo", @connection.select_value(query) @@ -239,7 +274,6 @@ module ActiveRecord def test_select_methods_passing_a_relation Post.create!(title: "foo", body: "bar") query = Post.where(title: "foo").select(:title) - assert_equal({ "title" => "foo" }, @connection.select_one(query.arel, nil, query.bound_attributes)) assert_equal({ "title" => "foo" }, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) assert_equal "foo", @connection.select_value(query) @@ -254,11 +288,11 @@ module ActiveRecord def test_log_invalid_encoding error = assert_raises RuntimeError do @connection.send :log, "SELECT 'Ñ‹' FROM DUAL" do - raise "Ñ‹".force_encoding(Encoding::ASCII_8BIT) + raise "Ñ‹".dup.force_encoding(Encoding::ASCII_8BIT) end end - assert_not_nil error.message + assert_equal "Ñ‹", error.message end end end @@ -346,5 +380,25 @@ module ActiveRecord assert !@connection.transaction_open? end end + + # test resetting sequences in odd tables in PostgreSQL + if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) + require "models/movie" + require "models/subscriber" + + def test_reset_empty_table_with_custom_pk + Movie.delete_all + Movie.connection.reset_pk_sequence! "movies" + assert_equal 1, Movie.create(name: "fight club").id + end + + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! "subscribers" + sub = Subscriber.new(name: "robert drake") + sub.id = "bob drake" + assert_nothing_raised { sub.save! } + end + end end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 67e1efde27..6931b085a8 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -7,7 +9,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def setup ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute - def execute(sql, name = nil) return sql end + def execute(sql, name = nil) sql end end end @@ -66,14 +68,14 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`)) ENGINE=InnoDB" + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))" actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, type: type end assert_equal expected, actual end - expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB" + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))" actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, length: 10, using: :btree end @@ -158,7 +160,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase 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) - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + 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 diff --git a/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb new file mode 100644 index 0000000000..4c67633946 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :auto_increments, if_exists: true + end + + def test_auto_increment_without_primary_key + @connection.create_table :auto_increments, id: false, force: true do |t| + t.integer :id, null: false, auto_increment: true + t.index :id + end + output = dump_table_schema("auto_increments") + assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output) + end + + def test_auto_increment_with_composite_primary_key + @connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t| + t.integer :id, null: false, auto_increment: true + t.datetime :created_at, null: false + end + output = dump_table_schema("auto_increments") + assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index 8f7c803a21..825bddfb73 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 58698d59db..db09b30361 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 50ba9ab831..fd5f712f1a 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index e4a6ed5482..d0c57de65d 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index a2faf43b0d..13b4096671 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -63,18 +65,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert @connection.active? end - def test_verify_with_args_is_deprecated - assert_deprecated do - @connection.verify!(option: true) - end - assert_deprecated do - @connection.verify!([]) - end - assert_deprecated do - @connection.verify!({}) - end - end - def test_execute_after_disconnect @connection.disconnect! @@ -184,10 +174,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal "SCHEMA", @subscriber.logged[0][1] end - def test_logs_name_rename_column_sql + def test_logs_name_rename_column_for_alter @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" @subscriber.logged.clear - @connection.send(:rename_column_sql, "bar_baz", "foo", "foo2") + @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2") assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" 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 c131a5169c..fa54f39992 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 7ad3e3ca2d..108bec832c 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2EnumTest < ActiveRecord::Mysql2TestCase diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 7916921e5a..b8e778f0b0 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain + explain = Author.where(id: 1).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain - assert_match %r(audit_logs |.* ALL), explain + explain = Author.where(id: 1).includes(:posts).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain + assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain + assert_match %r(posts |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 26c69edc7b..de78ba91f5 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "cases/json_shared_test_cases" diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 565130c38f..d18fb97e05 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/ddl_helper" diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 251a50e41e..62abd694bb 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + require "cases/helper" class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" @@ -31,6 +35,8 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase assert connection.column_exists?(table_name, :key, :string) end + ensure + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.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 1fad5585de..b587e756cf 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb index 4182532535..7b6dce71e9 100644 --- a/activerecord/test/cases/adapters/mysql2/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" @@ -15,7 +17,7 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase # Test that MySQL allows multiple results for stored procedures # # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. - # http://dev.mysql.com/doc/refman/5.6/en/call.html + # https://dev.mysql.com/doc/refman/5.6/en/call.html def test_multi_results rows = @connection.select_rows("CALL ten();") assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index d6e7f29a5c..e10642cbb4 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb index 61a8ce9bc0..1c92df940f 100644 --- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -15,28 +17,103 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase test "table options with ENGINE" do @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{ENGINE=MyISAM}, options end test "table options with ROW_FORMAT" do @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{ROW_FORMAT=REDUNDANT}, options end test "table options with CHARSET" do @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{CHARSET=utf8mb4}, options end test "table options with COLLATE" do @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{COLLATE=utf8mb4_bin}, options end end + +class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + def setup + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "schema dump includes ENGINE=InnoDB if not provided" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end + + test "schema dump includes ENGINE=InnoDB in legacy migrations" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end +end + +class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + def setup + @logger_was = ActiveRecord::Base.logger + @log = StringIO.new + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.logger = @logger_was + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "new migrations do not contain default ENGINE=InnoDB option" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + assert_no_match %r{ENGINE=InnoDB}, @log.string + end + + test "legacy migrations contain default ENGINE=InnoDB option" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_match %r{ENGINE=InnoDB}, @log.string + end +end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index 16101e38cb..f921515c10 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -11,6 +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) @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -29,6 +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) end test "raises Deadlocked when a deadlock is encountered" do @@ -57,5 +61,89 @@ module ActiveRecord end end end + + test "raises LockWaitTimeout when lock wait timeout exceeded" do + assert_raises(ActiveRecord::LockWaitTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET innodb_lock_wait_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET innodb_lock_wait_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises StatementTimeout when statement timeout exceeded" do + skip unless ActiveRecord::Base.connection.show_variable("max_execution_time") + assert_raises(ActiveRecord::StatementTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET max_execution_time = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET max_execution_time = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'") + conn.execute("KILL QUERY #{pid}") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end end end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index 71dcfaa241..b01f5d7f5a 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -60,7 +62,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase schema = dump_table_schema "unsigned_types" assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema - assert_match %r{t\.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t\.float\s+"unsigned_float",\s+unsigned: true$}, schema assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema 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 442a4fb7b5..ffde8ed4d8 100644 --- a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -52,7 +54,7 @@ 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\(`name`\)"$/i, output) + 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) 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 b787de8453..99c53dadeb 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase @@ -57,6 +59,9 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true) end + 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 }) + 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 121c62dadf..0e9e86f425 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -45,6 +47,39 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert ratings_column.array? end + def test_not_compatible_with_serialize_array + new_klass = Class.new(PgArray) do + serialize :tags, Array + end + assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do + new_klass.new + end + end + + class MyTags + def initialize(tags); @tags = tags end + def to_a; @tags end + def self.load(tags); new(tags) end + def self.dump(object); object.to_a end + end + + def test_array_with_serialized_attributes + new_klass = Class.new(PgArray) do + serialize :tags, MyTags + end + + new_klass.create!(tags: MyTags.new(["one", "two"])) + record = new_klass.first + + assert_instance_of MyTags, record.tags + assert_equal ["one", "two"], record.tags.to_a + + record.tags = MyTags.new(["three", "four"]) + record.save! + + assert_equal ["three", "four"], record.reload.tags.to_a + end + def test_default @connection.add_column "pg_arrays", "score", :integer, array: true, default: [4, 4, 2] PgArray.reset_column_information diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 7712e809a2..df04299569 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 65baed34e9..a6bee113ff 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb index 03b44feab6..305e033642 100644 --- a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index ea642069d2..adf461a9cc 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 52f2a0096c..f20958fbd2 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "ipaddr" diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index ca95e4b626..a25f102bad 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,78 +1,78 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" -if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase - include SchemaDumpingHelper - class Citext < ActiveRecord::Base - self.table_name = "citexts" - end +class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Citext < ActiveRecord::Base + self.table_name = "citexts" + end - def setup - @connection = ActiveRecord::Base.connection + def setup + @connection = ActiveRecord::Base.connection - enable_extension!("citext", @connection) + enable_extension!("citext", @connection) - @connection.create_table("citexts") do |t| - t.citext "cival" - end + @connection.create_table("citexts") do |t| + t.citext "cival" end + end - teardown do - @connection.drop_table "citexts", if_exists: true - disable_extension!("citext", @connection) - end + teardown do + @connection.drop_table "citexts", if_exists: true + disable_extension!("citext", @connection) + end - def test_citext_enabled - assert @connection.extension_enabled?("citext") - end + def test_citext_enabled + assert @connection.extension_enabled?("citext") + end - def test_column - column = Citext.columns_hash["cival"] - assert_equal :citext, column.type - assert_equal "citext", column.sql_type - assert_not column.array? + def test_column + column = Citext.columns_hash["cival"] + assert_equal :citext, column.type + assert_equal "citext", column.sql_type + assert_not column.array? - type = Citext.type_for_attribute("cival") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("citexts") do |t| - t.citext "username" - end - Citext.reset_column_information - column = Citext.columns_hash["username"] - assert_equal :citext, column.type + type = Citext.type_for_attribute("cival") + assert_not type.binary? + end - raise ActiveRecord::Rollback # reset the schema change + def test_change_table_supports_json + @connection.transaction do + @connection.change_table("citexts") do |t| + t.citext "username" end - ensure Citext.reset_column_information + column = Citext.columns_hash["username"] + assert_equal :citext, column.type + + raise ActiveRecord::Rollback # reset the schema change end + ensure + Citext.reset_column_information + end - def test_write - x = Citext.new(cival: "Some CI Text") - x.save! - citext = Citext.first - assert_equal "Some CI Text", citext.cival + def test_write + x = Citext.new(cival: "Some CI Text") + x.save! + citext = Citext.first + assert_equal "Some CI Text", citext.cival - citext.cival = "Some NEW CI Text" - citext.save! + citext.cival = "Some NEW CI Text" + citext.save! - assert_equal "Some NEW CI Text", citext.reload.cival - end + assert_equal "Some NEW CI Text", citext.reload.cival + end - def test_select_case_insensitive - @connection.execute "insert into citexts (cival) values('Cased Text')" - x = Citext.where(cival: "cased text").first - assert_equal "Cased Text", x.cival - end + def test_select_case_insensitive + @connection.execute "insert into citexts (cival) values('Cased Text')" + x = Citext.where(cival: "cased text").first + assert_equal "Cased Text", x.cival + end - def test_schema_dump_with_shorthand - output = dump_table_schema("citexts") - assert_match %r[t\.citext "cival"], output - end + def test_schema_dump_with_shorthand + output = dump_table_schema("citexts") + assert_match %r[t\.citext "cival"], output end end diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb index a603221d8f..7468f4c4f8 100644 --- a/activerecord/test/cases/adapters/postgresql/collation_test.rb +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 1da2a9e2ac..5da95f7e2c 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -104,7 +106,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase def setup super - @connection.type_map.register_type "full_address", FullAddressType.new + @connection.send(:type_map).register_type "full_address", FullAddressType.new end def test_column diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 32afe331fa..81358b8fc4 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -101,7 +103,7 @@ module ActiveRecord end def test_indexes_logs_name - assert_deprecated { @connection.indexes("items", "hello") } + @connection.indexes("items") assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -133,8 +135,8 @@ module ActiveRecord if ActiveRecord::Base.connection.prepared_statements def test_statement_key_is_logged - binds = [bind_attribute(nil, 1)] - @connection.exec_query("SELECT $1::integer", "SQL", binds, prepare: true) + bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) + @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true) name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") @@ -218,6 +220,13 @@ module ActiveRecord end end + def test_set_session_timezone + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { timezone: "America/New_York" })) + assert_equal "America/New_York", ActiveRecord::Base.connection.query_value("SHOW TIME ZONE") + end + end + def test_get_and_release_advisory_lock lock_id = 5295901941911233559 list_advisory_locks = <<-SQL diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 0725fde5ae..b7535d5c9a 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/ddl_helper" diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index f1eb8adb15..dafbc0a3db 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -42,6 +44,6 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase record.price = "34.15" record.save! - assert_equal BigDecimal.new("34.15"), record.reload.price + assert_equal BigDecimal("34.15"), record.reload.price end end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 5e5a3158ba..3d3cbe11a3 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index d79fbccf47..be525383e9 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + explain = Author.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(QUERY PLAN), explain end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain + explain = Author.where(id: 1).includes(:posts).explain assert_match %(QUERY PLAN), explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index b56c226763..df97ab11e7 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase @@ -20,10 +22,6 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase @connection = ActiveRecord::Base.connection - unless @connection.supports_extensions? - return skip("no extension support") - end - @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name @old_table_name_prefix = ActiveRecord::Base.table_name_prefix @old_table_name_suffix = ActiveRecord::Base.table_name_suffix diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index 5ddfe32007..c6f1e1727f 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 3b6840a1c9..e1ba00e07b 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index f9cce10fb8..f09e34b5f2 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,382 +1,378 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" -if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase - include SchemaDumpingHelper - class Hstore < ActiveRecord::Base - self.table_name = "hstores" +class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Hstore < ActiveRecord::Base + self.table_name = "hstores" - store_accessor :settings, :language, :timezone - end + store_accessor :settings, :language, :timezone + end - class FakeParameters - def to_unsafe_h - { "hi" => "hi" } - end + class FakeParameters + def to_unsafe_h + { "hi" => "hi" } end + end - def setup - @connection = ActiveRecord::Base.connection - - unless @connection.extension_enabled?("hstore") - @connection.enable_extension "hstore" - @connection.commit_db_transaction - end + def setup + @connection = ActiveRecord::Base.connection - @connection.reconnect! + enable_extension!("hstore", @connection) - @connection.transaction do - @connection.create_table("hstores") do |t| - t.hstore "tags", default: "" - t.hstore "payload", array: true - t.hstore "settings" - end + @connection.transaction do + @connection.create_table("hstores") do |t| + t.hstore "tags", default: "" + t.hstore "payload", array: true + t.hstore "settings" end - Hstore.reset_column_information - @column = Hstore.columns_hash["tags"] - @type = Hstore.type_for_attribute("tags") - end - - teardown do - @connection.drop_table "hstores", if_exists: true end + Hstore.reset_column_information + @column = Hstore.columns_hash["tags"] + @type = Hstore.type_for_attribute("tags") + end - def test_hstore_included_in_extensions - assert @connection.respond_to?(:extensions), "connection should have a list of extensions" - assert_includes @connection.extensions, "hstore", "extension list should include hstore" - end + teardown do + @connection.drop_table "hstores", if_exists: true + disable_extension!("hstore", @connection) + end - def test_disable_enable_hstore - assert @connection.extension_enabled?("hstore") - @connection.disable_extension "hstore" - assert_not @connection.extension_enabled?("hstore") - @connection.enable_extension "hstore" - assert @connection.extension_enabled?("hstore") - ensure - # Restore column(s) dropped by `drop extension hstore cascade;` - load_schema - end + def test_hstore_included_in_extensions + assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert_includes @connection.extensions, "hstore", "extension list should include hstore" + end - def test_column - assert_equal :hstore, @column.type - assert_equal "hstore", @column.sql_type - assert_not @column.array? + def test_disable_enable_hstore + assert @connection.extension_enabled?("hstore") + @connection.disable_extension "hstore" + assert_not @connection.extension_enabled?("hstore") + @connection.enable_extension "hstore" + assert @connection.extension_enabled?("hstore") + ensure + # Restore column(s) dropped by `drop extension hstore cascade;` + load_schema + end - assert_not @type.binary? - end + def test_column + assert_equal :hstore, @column.type + assert_equal "hstore", @column.sql_type + assert_not @column.array? - def test_default - @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"' - Hstore.reset_column_information + assert_not @type.binary? + end - assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"]) - assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions) - ensure - Hstore.reset_column_information - end + def test_default + @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"' + Hstore.reset_column_information - def test_change_table_supports_hstore - @connection.transaction do - @connection.change_table("hstores") do |t| - t.hstore "users", default: "" - end - Hstore.reset_column_information - column = Hstore.columns_hash["users"] - assert_equal :hstore, column.type + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"]) + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions) + ensure + Hstore.reset_column_information + end - raise ActiveRecord::Rollback # reset the schema change + def test_change_table_supports_hstore + @connection.transaction do + @connection.change_table("hstores") do |t| + t.hstore "users", default: "" end - ensure Hstore.reset_column_information + column = Hstore.columns_hash["users"] + assert_equal :hstore, column.type + + raise ActiveRecord::Rollback # reset the schema change end + ensure + Hstore.reset_column_information + end - def test_hstore_migration - hstore_migration = Class.new(ActiveRecord::Migration::Current) do - def change - change_table("hstores") do |t| - t.hstore :keys - end + def test_hstore_migration + hstore_migration = Class.new(ActiveRecord::Migration::Current) do + def change + change_table("hstores") do |t| + t.hstore :keys end end - - hstore_migration.new.suppress_messages do - hstore_migration.migrate(:up) - assert_includes @connection.columns(:hstores).map(&:name), "keys" - hstore_migration.migrate(:down) - assert_not_includes @connection.columns(:hstores).map(&:name), "keys" - end end - def test_cast_value_on_write - x = Hstore.new tags: { "bool" => true, "number" => 5 } - assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast) - assert_equal({ "bool" => "true", "number" => "5" }, x.tags) - x.save - assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags) + hstore_migration.new.suppress_messages do + hstore_migration.migrate(:up) + assert_includes @connection.columns(:hstores).map(&:name), "keys" + hstore_migration.migrate(:down) + assert_not_includes @connection.columns(:hstores).map(&:name), "keys" end + end - def test_type_cast_hstore - assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\"")) - assert_equal({}, @type.deserialize("")) - assert_equal({ "key" => nil }, @type.deserialize("key => NULL")) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) - end + def test_cast_value_on_write + x = Hstore.new tags: { "bool" => true, "number" => 5 } + assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast) + assert_equal({ "bool" => "true", "number" => "5" }, x.tags) + x.save + assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags) + end - def test_with_store_accessors - x = Hstore.new(language: "fr", timezone: "GMT") - assert_equal "fr", x.language - assert_equal "GMT", x.timezone + def test_type_cast_hstore + assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\"")) + assert_equal({}, @type.deserialize("")) + assert_equal({ "key" => nil }, @type.deserialize("key => NULL")) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) + end - x.save! - x = Hstore.first - assert_equal "fr", x.language - assert_equal "GMT", x.timezone + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - x.language = "de" - x.save! + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - x = Hstore.first - assert_equal "de", x.language - assert_equal "GMT", x.timezone - end + x.language = "de" + x.save! - def test_duplication_with_store_accessors - x = Hstore.new(language: "fr", timezone: "GMT") - assert_equal "fr", x.language - assert_equal "GMT", x.timezone + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end - y = x.dup - assert_equal "fr", y.language - assert_equal "GMT", y.timezone - end + def test_duplication_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_yaml_round_trip_with_store_accessors - x = Hstore.new(language: "fr", timezone: "GMT") - assert_equal "fr", x.language - assert_equal "GMT", x.timezone + y = x.dup + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end - y = YAML.load(YAML.dump(x)) - assert_equal "fr", y.language - assert_equal "GMT", y.timezone - end + def test_yaml_round_trip_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_changes_in_place - hstore = Hstore.create!(settings: { "one" => "two" }) - hstore.settings["three"] = "four" - hstore.save! - hstore.reload + y = YAML.load(YAML.dump(x)) + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end - assert_equal "four", hstore.settings["three"] - assert_not hstore.changed? - end + def test_changes_in_place + hstore = Hstore.create!(settings: { "one" => "two" }) + hstore.settings["three"] = "four" + hstore.save! + hstore.reload - def test_dirty_from_user_equal - settings = { "alongkey" => "anything", "key" => "value" } - hstore = Hstore.create!(settings: settings) + assert_equal "four", hstore.settings["three"] + assert_not hstore.changed? + end - hstore.settings = { "key" => "value", "alongkey" => "anything" } - assert_equal settings, hstore.settings - refute hstore.changed? - end + def test_dirty_from_user_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) - def test_hstore_dirty_from_database_equal - settings = { "alongkey" => "anything", "key" => "value" } - hstore = Hstore.create!(settings: settings) - hstore.reload + hstore.settings = { "key" => "value", "alongkey" => "anything" } + assert_equal settings, hstore.settings + refute hstore.changed? + end - assert_equal settings, hstore.settings - hstore.settings = settings - refute hstore.changed? - end + def test_hstore_dirty_from_database_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + hstore.reload - def test_gen1 - assert_equal('" "=>""', @type.serialize(" " => "")) - end + assert_equal settings, hstore.settings + hstore.settings = settings + refute hstore.changed? + end - def test_gen2 - assert_equal('","=>""', @type.serialize("," => "")) - end + def test_gen1 + assert_equal('" "=>""', @type.serialize(" " => "")) + end - def test_gen3 - assert_equal('"="=>""', @type.serialize("=" => "")) - end + def test_gen2 + assert_equal('","=>""', @type.serialize("," => "")) + end - def test_gen4 - assert_equal('">"=>""', @type.serialize(">" => "")) - end + def test_gen3 + assert_equal('"="=>""', @type.serialize("=" => "")) + end - def test_parse1 - assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) - end + def test_gen4 + assert_equal('">"=>""', @type.serialize(">" => "")) + end - def test_parse2 - assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ ")) - end + def test_parse1 + assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + end - def test_parse3 - assert_equal({ "=" => ">" }, @type.deserialize("==>>")) - end + def test_parse2 + assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ ")) + end - def test_parse4 - assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w')) - end + def test_parse3 + assert_equal({ "=" => ">" }, @type.deserialize("==>>")) + end - def test_parse5 - assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w')) - end + def test_parse4 + assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w')) + end - def test_parse6 - assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w')) - end + def test_parse5 + assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w')) + end - def test_parse7 - assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w')) - end + def test_parse6 + assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w')) + end - def test_rewrite - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - x.tags = { '"a\'' => "b" } - assert x.save! - end + def test_parse7 + assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w')) + end - def test_select - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - assert_equal({ "1" => "2" }, x.tags) - end + def test_rewrite + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + x.tags = { '"a\'' => "b" } + assert x.save! + end - def test_array_cycle - assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }]) - end + def test_select + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + assert_equal({ "1" => "2" }, x.tags) + end - def test_array_strings_with_quotes - assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }]) - end + def test_array_cycle + assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }]) + end - def test_array_strings_with_commas - assert_array_cycle([{ "this,has" => "many,values" }]) - end + def test_array_strings_with_quotes + assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }]) + end - def test_array_strings_with_array_delimiters - assert_array_cycle(["{" => "}"]) - end + def test_array_strings_with_commas + assert_array_cycle([{ "this,has" => "many,values" }]) + end - def test_array_strings_with_null_strings - assert_array_cycle([{ "NULL" => "NULL" }]) - end + def test_array_strings_with_array_delimiters + assert_array_cycle(["{" => "}"]) + end - def test_contains_nils - assert_array_cycle([{ "NULL" => nil }]) - end + def test_array_strings_with_null_strings + assert_array_cycle([{ "NULL" => "NULL" }]) + end - def test_select_multikey - @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.first - assert_equal({ "1" => "2", "2" => "3" }, x.tags) - end + def test_contains_nils + assert_array_cycle([{ "NULL" => nil }]) + end - def test_create - assert_cycle("a" => "b", "1" => "2") - end + def test_select_multikey + @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" + x = Hstore.first + assert_equal({ "1" => "2", "2" => "3" }, x.tags) + end - def test_nil - assert_cycle("a" => nil) - end + def test_create + assert_cycle("a" => "b", "1" => "2") + end - def test_quotes - assert_cycle("a" => 'b"ar', '1"foo' => "2") - end + def test_nil + assert_cycle("a" => nil) + end - def test_whitespace - assert_cycle("a b" => "b ar", '1"foo' => "2") - end + def test_quotes + assert_cycle("a" => 'b"ar', '1"foo' => "2") + end - def test_backslash - assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2") - end + def test_whitespace + assert_cycle("a b" => "b ar", '1"foo' => "2") + end - def test_comma - assert_cycle("a, b" => "bar", '1"foo' => "2") - end + def test_backslash + assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2") + end - def test_arrow - assert_cycle("a=>b" => "bar", '1"foo' => "2") - end + def test_comma + assert_cycle("a, b" => "bar", '1"foo' => "2") + end - def test_quoting_special_characters - assert_cycle("ca" => "cà ", "ac" => "à c") - end + def test_arrow + assert_cycle("a=>b" => "bar", '1"foo' => "2") + end - def test_multiline - assert_cycle("a\nb" => "c\nd") - end + def test_quoting_special_characters + assert_cycle("ca" => "cà ", "ac" => "à c") + end - class TagCollection - def initialize(hash); @hash = hash end - def to_hash; @hash end - def self.load(hash); new(hash) end - def self.dump(object); object.to_hash end - end + def test_multiline + assert_cycle("a\nb" => "c\nd") + end - class HstoreWithSerialize < Hstore - serialize :tags, TagCollection - end + class TagCollection + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end - def test_hstore_with_serialized_attributes - HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") - record = HstoreWithSerialize.first - assert_instance_of TagCollection, record.tags - assert_equal({ "one" => "two" }, record.tags.to_hash) - record.tags = TagCollection.new("three" => "four") - record.save! - assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash) - end + class HstoreWithSerialize < Hstore + serialize :tags, TagCollection + end - def test_clone_hstore_with_serialized_attributes - HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") - record = HstoreWithSerialize.first - dupe = record.dup - assert_equal({ "one" => "two" }, dupe.tags.to_hash) - end + def test_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") + record = HstoreWithSerialize.first + assert_instance_of TagCollection, record.tags + assert_equal({ "one" => "two" }, record.tags.to_hash) + record.tags = TagCollection.new("three" => "four") + record.save! + assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash) + end - def test_schema_dump_with_shorthand - output = dump_table_schema("hstores") - assert_match %r[t\.hstore "tags",\s+default: {}], output - end + def test_clone_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") + record = HstoreWithSerialize.first + dupe = record.dup + assert_equal({ "one" => "two" }, dupe.tags.to_hash) + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("hstores") + assert_match %r[t\.hstore "tags",\s+default: {}], output + end + + def test_supports_to_unsafe_h_values + assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + end - def test_supports_to_unsafe_h_values - assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + private + def assert_array_cycle(array) + # test creation + x = Hstore.create!(payload: array) + x.reload + assert_equal(array, x.payload) + + # test updating + x = Hstore.create!(payload: []) + x.payload = array + x.save! + x.reload + assert_equal(array, x.payload) end - private - def assert_array_cycle(array) - # test creation - x = Hstore.create!(payload: array) - x.reload - assert_equal(array, x.payload) - - # test updating - x = Hstore.create!(payload: []) - x.payload = array - x.save! - x.reload - assert_equal(array, x.payload) - end + def assert_cycle(hash) + # test creation + x = Hstore.create!(tags: hash) + x.reload + assert_equal(hash, x.tags) - def assert_cycle(hash) - # test creation - x = Hstore.create!(tags: hash) - x.reload - assert_equal(hash, x.tags) - - # test updating - x = Hstore.create!(tags: {}) - x.tags = hash - x.save! - x.reload - assert_equal(hash, x.tags) - end - end + # test updating + x = Hstore.create!(tags: {}) + x.tags = hash + x.save! + x.reload + assert_equal(hash, x.tags) + end end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index b9e177e6ec..0b18c0c9d7 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index b4e55964b9..3e45b057ff 100644 --- a/activerecord/test/cases/adapters/postgresql/integer_test.rb +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/numeric/bytes" diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 6aa60630c2..ee08841eb3 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "cases/json_shared_test_cases" @@ -19,8 +21,8 @@ module PostgresqlJSONSharedTestCases @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } klass.reset_column_information - assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) - assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions) end def test_deserialize_with_array diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 2b5ac1cac6..eca29f2892 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index ea060345a7..cc10890fa8 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -31,8 +33,8 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase end def test_default - assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults["depth"] - assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth + assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] + assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth end def test_money_values @@ -63,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase money = PostgresqlMoney.create(wealth: "987.65".dup) assert_equal 987.65, money.wealth - new_value = BigDecimal.new("123.45") + new_value = BigDecimal("123.45") money.wealth = new_value money.save! money.reload diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index a33b0ef8a7..f461544a85 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index bfb2b7c27a..b53a12254d 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 76e0ad60fe..1951230c8a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/ddl_helper" require "support/connection_helper" @@ -202,8 +204,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - binds = [bind_attribute("id", 1)] - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) + bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -217,8 +219,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - binds = [bind_attribute("id", "1-fuu", Type::Integer.new)] - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) + bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -246,12 +248,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops" - index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } - assert_equal "data varchar_pattern_ops", index.columns + @connection.add_index "ex", "data", opclass: "varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } + assert_equal ["data"], index.columns - @connection.remove_index "ex", "data varchar_pattern_ops" - assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } + @connection.remove_index "ex", "data" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } end end @@ -325,15 +327,18 @@ module ActiveRecord end def test_only_reload_type_map_once_for_every_unrecognized_type + reset_connection + connection = ActiveRecord::Base.connection + silence_warnings do assert_queries 2, ignore_none: true do - @connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 1, ignore_none: true do - @connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 2, ignore_none: true do - @connection.select_all "SELECT NULL::anyarray" + connection.select_all "SELECT NULL::anyarray" end end ensure @@ -341,10 +346,13 @@ module ActiveRecord end def test_only_warn_on_first_encounter_of_unrecognized_oid + reset_connection + connection = ActiveRecord::Base.connection + warning = capture(:stderr) { - @connection.select_all "select 'pg_catalog.pg_class'::regclass" - @connection.select_all "select 'pg_catalog.pg_class'::regclass" - @connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" } assert_match(/\Aunknown OID \d+: failed to recognize type of 'regclass'\. It will be treated as String\.\n\z/, warning) ensure diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb index 8c62690866..f7478b50c3 100644 --- a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/computer" require "models/developer" diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index a1e966b915..d50dc49276 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -9,11 +11,11 @@ module ActiveRecord end def test_type_cast_true - assert_equal "t", @conn.type_cast(true) + assert_equal true, @conn.type_cast(true) end def test_type_cast_false - assert_equal "f", @conn.type_cast(false) + assert_equal false, @conn.type_cast(false) end def test_quote_float_nan diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index f411884dfd..261c24634e 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" @@ -132,10 +134,10 @@ _SQL end def test_numrange_values - assert_equal BigDecimal.new("0.1")..BigDecimal.new("0.2"), @first_range.num_range - assert_equal BigDecimal.new("0.1")...BigDecimal.new("0.2"), @second_range.num_range - assert_equal BigDecimal.new("0.1")...BigDecimal.new("Infinity"), @third_range.num_range - assert_equal BigDecimal.new("-Infinity")...BigDecimal.new("Infinity"), @fourth_range.num_range + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range assert_nil @empty_range.num_range end @@ -230,16 +232,67 @@ _SQL end end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end + + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end + + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end + + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end + + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("1")) + BigDecimal("0.5")...BigDecimal("1")) end def test_update_numrange assert_equal_round_trip(@first_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("1")) + BigDecimal("0.5")...BigDecimal("1")) assert_nil_round_trip(@first_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("0.5")) + BigDecimal("0.5")...BigDecimal("0.5")) end def test_create_daterange @@ -305,6 +358,18 @@ _SQL end end + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) + + record = PostgresqlRange.first + + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index 0ff04bfa27..0bcc214c24 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index e9e7f717ac..100d247113 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index f86a76e08a..fcb0aec81b 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class SchemaThing < ActiveRecord::Base @@ -68,7 +70,7 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase USERS.each do |u| @connection.clear_cache! set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_attribute("id", 1)]) + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)]) set_session_auth end end @@ -101,4 +103,8 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase def set_session_auth(auth = nil) @connection.session_auth = auth || "default" end + + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index f6b957476b..2c99fa78bd 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/default" require "support/schema_dumping_helper" @@ -169,17 +171,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", "sql", [bind_attribute("id", 1)] + @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] end end if ActiveRecord::Base.connection.prepared_statements def test_schema_change_with_prepared_stmt altered = false - @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] @connection.exec_query "alter table developers add column zomg int", "sql", [] altered = true - @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @@ -457,7 +459,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal :btree, index_d.using assert_equal :gin, index_e.using - assert_equal :desc, index_d.orders[INDEX_D_COLUMN] + assert_equal :desc, index_d.orders end end @@ -467,6 +469,10 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end + + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end end class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase @@ -494,6 +500,38 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase end end +class SchemaIndexOpclassTest < 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_string_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :text_pattern_ops/, output) + end + + def test_non_default_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) + end +end + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection @@ -528,7 +566,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa end def test_decimal_defaults_in_new_schema_when_overriding_domain - assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" end def test_bpchar_defaults_in_new_schema_when_overriding_domain diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index d711b3b729..6a99323be5 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -84,3 +86,71 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output end end + +module SequenceNameDetectionTestCases + class CollidedSequenceNameTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :foo_bar, force: true do |t| + t.serial :baz_id + end + @connection.create_table :foo, force: true do |t| + t.serial :bar_id + t.bigserial :bar_baz_id + end + end + + def teardown + @connection.drop_table :foo_bar, if_exists: true + @connection.drop_table :foo, if_exists: true + end + + def test_serial_columns + columns = @connection.columns(:foo) + columns.each do |column| + assert_equal :integer, column.type + assert column.serial? + end + end + + def test_schema_dump_with_collided_sequence_name + output = dump_table_schema "foo" + assert_match %r{t\.serial\s+"bar_id",\s+null: false$}, output + assert_match %r{t\.bigserial\s+"bar_baz_id",\s+null: false$}, output + end + end + + class LongerSequenceNameDetectionTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @table_name = "long_table_name_to_test_sequence_name_detection_for_serial_cols" + @connection = ActiveRecord::Base.connection + @connection.create_table @table_name, force: true do |t| + t.serial :seq + t.bigserial :bigseq + end + end + + def teardown + @connection.drop_table @table_name, if_exists: true + end + + def test_serial_columns + columns = @connection.columns(@table_name) + columns.each do |column| + assert_equal :integer, column.type + assert column.serial? + end + end + + def test_schema_dump_with_long_table_name + output = dump_table_schema @table_name + assert_match %r{create_table "#{@table_name}", force: :cascade}, output + assert_match %r{t\.serial\s+"seq",\s+null: false$}, output + assert_match %r{t\.bigserial\s+"bigseq",\s+null: false$}, output + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index 146b619a4b..fef4b02b04 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -21,7 +23,7 @@ module ActiveRecord assert_equal "bar", cache["foo"] pid = fork { - lookup = cache["foo"]; + lookup = cache["foo"] exit!(!lookup) } diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 962450aada..b7f213efc8 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/topic" diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 9b42d0383d..9821b103df 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" require "concurrent/atomic/cyclic_barrier" @@ -12,6 +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) @connection = ActiveRecord::Base.connection @@ -29,6 +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) end test "raises SerializationFailure when a serialization failure occurs" do @@ -89,6 +93,90 @@ module ActiveRecord end end + test "raises LockWaitTimeout when lock wait timeout exceeded" do + skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 + assert_raises(ActiveRecord::LockWaitTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET lock_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET lock_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when statement timeout exceeded" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET statement_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET statement_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'") + conn.execute("SELECT pg_cancel_backend(#{pid})") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end + private def with_warning_suppression diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index 784d77a8d1..8212ed4263 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase @@ -6,16 +8,16 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase end test "array delimiters are looked up correctly" do - box_array = @connection.type_map.lookup(1020) - int_array = @connection.type_map.lookup(1007) + box_array = @connection.send(:type_map).lookup(1020) + int_array = @connection.send(:type_map).lookup(1007) assert_equal ";", box_array.delimiter assert_equal ",", int_array.delimiter end test "array types correctly respect registration of subtypes" do - int_array = @connection.type_map.lookup(1007, -1, "integer[]") - bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") + int_array = @connection.send(:type_map).lookup(1007, -1, "integer[]") + bigint_array = @connection.send(:type_map).lookup(1016, -1, "bigint[]") big_array = [123456789123456789] assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } @@ -23,11 +25,11 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase end test "range types correctly respect registration of subtypes" do - int_range = @connection.type_map.lookup(3904, -1, "int4range") - bigint_range = @connection.type_map.lookup(3926, -1, "int8range") + int_range = @connection.send(:type_map).lookup(3904, -1, "int4range") + bigint_range = @connection.send(:type_map).lookup(3926, -1, "int8range") big_range = 0..123456789123456789 assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } - assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) + assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range)) end end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 9f9e3bda2f..c91884f384 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/connection_adapters/postgresql/utils" diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 8eddd81c38..c24e0cb330 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -74,6 +76,19 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase assert_nil column.default end + def test_add_column_with_default_array + connection.add_column :uuid_data_type, :thingy, :uuid, array: true, default: [] + + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + + assert column.array? + assert_equal "{}", column.default + + schema = dump_table_schema "uuid_data_type" + assert_match %r{t\.uuid "thingy", default: \[\], array: true$}, schema + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type @@ -124,7 +139,9 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11", "a0eebc999r0b4ef8ab6d6bb9bd380a11", "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11", - "{a0eebc99-bb6d6bb9-bd380a11}"].each do |invalid_uuid| + "{a0eebc99-bb6d6bb9-bd380a11}", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11", + "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid| uuid = UUIDType.new guid: invalid_uuid assert_nil uuid.guid end @@ -205,68 +222,66 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();" end - if ActiveRecord::Base.connection.supports_extensions? - def test_id_is_uuid - assert_equal :uuid, UUID.columns_hash["id"].type - assert UUID.primary_key - end + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash["id"].type + assert UUID.primary_key + end - def test_id_has_a_default - u = UUID.create - assert_not_nil u.id - end + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end - def test_auto_create_uuid - u = UUID.create - u.reload - assert_not_nil u.other_uuid - end + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end - def test_pk_and_sequence_for_uuid_primary_key - pk, seq = connection.pk_and_sequence_for("pg_uuids") - assert_equal "id", pk - assert_nil seq - end + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = connection.pk_and_sequence_for("pg_uuids") + assert_equal "id", pk + assert_nil seq + end - def test_schema_dumper_for_uuid_primary_key - schema = dump_table_schema "pg_uuids" - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema) - assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema) - end + def test_schema_dumper_for_uuid_primary_key + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema) + end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = dump_table_schema "pg_uuids_2" + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) + end - def test_schema_dumper_for_uuid_primary_key_with_custom_default - schema = dump_table_schema "pg_uuids_2" - assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) - assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) + def test_schema_dumper_for_uuid_primary_key_default + schema = dump_table_schema "pg_uuids_3" + if connection.supports_pgcrypto_uuid? + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema) + else + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) end + end + + def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false - def test_schema_dumper_for_uuid_primary_key_default - schema = dump_table_schema "pg_uuids_3" - if connection.supports_pgcrypto_uuid? - assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema) - else - assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid) end - end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate - def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration - @verbose_was = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migration = Class.new(ActiveRecord::Migration[5.0]) do - def version; 101 end - def migrate(x) - create_table("pg_uuids_4", id: :uuid) - end - end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate - - schema = dump_table_schema "pg_uuids_4" - assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) - ensure - drop_table "pg_uuids_4" - ActiveRecord::Migration.verbose = @verbose_was - end + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was end end @@ -285,38 +300,36 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase drop_table "pg_uuids" end - if ActiveRecord::Base.connection.supports_extensions? - def test_id_allows_default_override_via_nil - col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default - FROM pg_attribute a - LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first - assert_nil col_desc["default"] - end + def test_id_allows_default_override_via_nil + col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + FROM pg_attribute a + LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first + assert_nil col_desc["default"] + end - def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil - schema = dump_table_schema "pg_uuids" - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) - end + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end - def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration - @verbose_was = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migration = Class.new(ActiveRecord::Migration[5.0]) do - def version; 101 end - def migrate(x) - create_table("pg_uuids_4", id: :uuid, default: nil) - end - end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate - - schema = dump_table_schema "pg_uuids_4" - assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) - ensure - drop_table "pg_uuids_4" - ActiveRecord::Migration.verbose = @verbose_was - end + def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid, default: nil) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was end end @@ -350,23 +363,21 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase drop_table "pg_uuid_posts" end - if ActiveRecord::Base.connection.supports_extensions? - def test_collection_association_with_uuid - post = UuidPost.create! - comment = post.uuid_comments.create! - assert post.uuid_comments.find(comment.id) - end + def test_collection_association_with_uuid + post = UuidPost.create! + comment = post.uuid_comments.create! + assert post.uuid_comments.find(comment.id) + end - def test_find_with_uuid - UuidPost.create! - assert_raise ActiveRecord::RecordNotFound do - UuidPost.find(123456) - end + def test_find_with_uuid + UuidPost.create! + assert_raise ActiveRecord::RecordNotFound do + UuidPost.find(123456) end + end - def test_find_by_with_uuid - UuidPost.create! - assert_nil UuidPost.find_by(id: 789) - end + def test_find_by_with_uuid + UuidPost.create! + assert_nil UuidPost.find_by(id: 789) end end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 826b384fb3..71ead6f7f3 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index dd88ed3656..76c8f7d8dd 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index e1cfd703e8..ffb1d6afce 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class CopyTableTest < ActiveRecord::SQLite3TestCase diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 29d97ae78c..b6d2ccdb53 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) + explain = Author.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain - assert_match(/(SCAN )?TABLE audit_logs/, explain) + explain = Author.where(id: 1).includes(:posts).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain) + assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\? \[\["author_id", 1\]\]|1)), explain + assert_match(/(SCAN )?TABLE posts/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb new file mode 100644 index 0000000000..6f247fcd22 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "cases/helper" +require "cases/json_shared_test_cases" + +class SQLite3JSONTest < ActiveRecord::SQLite3TestCase + include JSONSharedTestCases + + def setup + super + @connection.create_table("json_data_type") do |t| + t.json "payload", default: {} + t.json "settings" + end + end + + def test_default + @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } + klass.reset_column_information + + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions) + end + + private + def column_type + :json + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index aefbb309e6..6fdb353368 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "bigdecimal" require "securerandom" @@ -5,6 +7,11 @@ require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase def setup @conn = ActiveRecord::Base.connection + @initial_represent_boolean_as_integer = ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + end + + def teardown + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = @initial_represent_boolean_as_integer end def test_type_cast_binary_encoding_without_logger @@ -15,15 +22,23 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase end def test_type_cast_true + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false assert_equal "t", @conn.type_cast(true) + + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true + assert_equal 1, @conn.type_cast(true) end def test_type_cast_false + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false assert_equal "f", @conn.type_cast(false) + + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true + assert_equal 0, @conn.type_cast(false) end def test_type_cast_bigdecimal - bd = BigDecimal.new "10.0" + bd = BigDecimal "10.0" assert_equal bd.to_f, @conn.type_cast(bd) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index cf0c37f70c..20579c6476 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/owner" require "tempfile" @@ -66,11 +68,11 @@ module ActiveRecord def test_exec_insert with_example_table do - binds = [bind_attribute("number", 10)] - @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", binds) + vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)] + @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals) result = @conn.exec_query( - "select number from ex where number = ?", "SQL", binds) + "select number from ex where number = ?", "SQL", vals) assert_equal 1, result.rows.length assert_equal 10, result.rows.first.first @@ -134,7 +136,7 @@ module ActiveRecord with_example_table "id int, data string" do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", 1)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -148,7 +150,7 @@ module ActiveRecord @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", "1-fuu", Type::Integer.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -267,14 +269,6 @@ module ActiveRecord end end - def test_indexes_logs_name - with_example_table do - assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do - assert_deprecated { @conn.indexes("ex", "hello") } - end - end - end - def test_table_exists_logs_name with_example_table do sql = <<-SQL @@ -366,6 +360,116 @@ module ActiveRecord end end + class Barcode < ActiveRecord::Base + self.primary_key = "code" + end + + def test_copy_table_with_existing_records_have_custom_primary_key + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t| + t.text :other_attr + end + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create!(code: code, other_attr: "xxx") + + connection.remove_column("barcodes", "other_attr") + + assert_equal code, Barcode.first.id + ensure + Barcode.reset_column_information + end + + def test_copy_table_with_composite_primary_keys + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.string :code + t.text :other_attr + end + region = "US" + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create!(region: region, code: code, other_attr: "xxx") + + connection.remove_column("barcodes", "other_attr") + + assert_equal ["region", "code"], connection.primary_keys("barcodes") + + barcode = Barcode.first + assert_equal region, barcode.region + assert_equal code, barcode.code + ensure + Barcode.reset_column_information + end + + def test_custom_primary_key_in_create_table + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.primary_key :id, :string + end + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_custom_primary_key_in_change_table + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.integer :dummy + end + connection.change_table :barcodes do |t| + t.primary_key :id, :string + end + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_add_column_with_custom_primary_key + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.integer :dummy + end + connection.add_column :barcodes, :id, :string, primary_key: true + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_remove_column_preserves_partial_indexes + connection = Barcode.connection + connection.create_table :barcodes, force: true do |t| + t.string :code + t.string :region + t.boolean :bool_attr + + t.index :code, unique: true, where: :bool_attr, name: "partial" + end + connection.remove_column :barcodes, :region + + index = connection.indexes("barcodes").find { |idx| idx.name == "partial" } + assert_equal "bool_attr", index.where + ensure + Barcode.reset_column_information + end + def test_supports_extensions assert_not @conn.supports_extensions?, "does not support extensions" end @@ -396,6 +500,10 @@ module ActiveRecord end end + def test_deprecate_valid_alter_table_type + assert_deprecated { @conn.valid_alter_table_type?(:string) } + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index b1b4463bf1..d70486605f 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/owner" diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index 37ff973397..61002435a4 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase @@ -8,7 +10,7 @@ class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase assert_equal "bar", cache["foo"] pid = fork { - lookup = cache["foo"]; + lookup = cache["foo"] exit!(!lookup) } diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index f8136fde72..fbdf2ada4b 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/customer" @@ -25,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase def test_immutable_value_objects customers(:david).balance = Money.new(100) - assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } } + assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } } end def test_inferred_mapping diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 5b608d8e83..83974f327e 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class ActiveRecordSchemaTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb deleted file mode 100644 index c54542ff7b..0000000000 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "cases/helper" -require "models/post" -require "models/author" - -module ActiveRecord - module Associations - class AssociationScopeTest < ActiveRecord::TestCase - test "does not duplicate conditions" do - scope = AssociationScope.scope(Author.new.association(:welcome_posts)) - binds = scope.where_clause.binds.map(&:value) - assert_equal binds.uniq, binds - 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 a727cc6e60..0f7a249bf3 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/project" @@ -215,6 +217,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id) drop_table :admin_regions, if_exists: true end + + Admin::User.reset_column_information end def test_natural_assignment @@ -647,8 +651,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_new_record_with_foreign_key_but_no_object client = Client.new("firm_id" => 1) - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.all.merge!(order: "id").first, client.firm_with_basic_id + assert_equal Firm.first, client.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded @@ -1168,6 +1171,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Column.create! record: record assert_equal 1, Column.count end + + def test_multiple_counter_cache_with_after_create_update + post = posts(:welcome) + parent = comments(:greetings) + + assert_difference "parent.reload.children_count", +1 do + assert_difference "post.reload.comments_count", +1 do + CommentWithAfterCreateUpdate.create(body: "foo", post: post, parent: parent) + end + end + end end class BelongsToWithForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb index 8a0e041864..88221b012e 100644 --- a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb +++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/content" diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index f9d1e44595..e096cd4a0b 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/author" diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 7b0445025c..e717621928 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -35,7 +37,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a - assert_equal 1, assert_no_queries { authors.size } + assert_equal 3, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end @@ -134,7 +136,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.all.merge!(includes: { posts: [ :special_comments , :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first + author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments 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 61f39b4136..8754889143 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/tagging" diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index e9f551b6b2..c5b2b77bd4 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/tag" diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 16eff15026..420a5a805b 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class EagerSingularizationTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index c0bab19e82..9830917bc3 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/tagging" @@ -38,6 +40,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nil member.favourite_club end + def test_should_work_inverse_of_with_eager_load + author = authors(:david) + assert_same author, author.posts.first.author + assert_same author, author.posts.eager_load(:comments).first.author + end + def test_loading_with_one_association posts = Post.all.merge!(includes: :comments).to_a post = posts.find { |p| p.id == 1 } @@ -419,7 +427,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") assert_nothing_raised do - Comment.includes(:post).references(:posts).order(quoted_posts_id) + Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id)) end end @@ -522,6 +530,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments } end + def test_preloading_has_many_through_with_implicit_source + authors = Author.includes(:very_special_comments).to_a + assert_no_queries do + special_comment_authors = authors.map { |author| [author.name, author.very_special_comments.size] } + assert_equal [["David", 1], ["Mary", 0], ["Bob", 0]], special_comment_authors + end + end + def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both author = Author.all.merge!(includes: :special_nonexistent_post_comments, order: "authors.id").first assert_equal [], author.special_nonexistent_post_comments @@ -771,7 +787,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag) tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) - assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title) end def test_eager_has_many_through_multiple_with_order @@ -853,23 +869,19 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def find_all_ordered(className, include = nil) - className.all.merge!(order: "#{className.table_name}.#{className.primary_key}", includes: include).to_a - end - def test_limited_eager_with_order assert_equal( posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: "UPPER(posts.title)", limit: 2, offset: 1 + order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: "UPPER(posts.title) DESC", limit: 2, offset: 1 + order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1 ).to_a ) end @@ -879,14 +891,14 @@ class EagerAssociationTest < ActiveRecord::TestCase posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: ["UPPER(posts.title)", "posts.id"], limit: 2, offset: 1 + order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: ["UPPER(posts.title) DESC", "posts.id"], limit: 2, offset: 1 + order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1 ).to_a ) end @@ -1061,7 +1073,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_load_with_sti_sharing_association - assert_queries(2) do #should not do 1 query per subclass + assert_queries(2) do # should not do 1 query per subclass Comment.includes(:post).to_a end end @@ -1291,6 +1303,11 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts end + def test_preloading_has_many_through_with_custom_scope + project = Project.includes(:developers_named_david_with_hash_conditions).find(projects(:active_record).id) + assert_equal [developers(:david)], project.developers_named_david_with_hash_conditions + end + test "scoping with a circular preload" do assert_equal Comment.find(1), Comment.preload(post: :comments).scoping { Comment.find(1) } end @@ -1484,9 +1501,18 @@ 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? + end + # CollectionProxy#reader is expensive, so the preloader avoids calling it. test "preloading has_many_through association avoids calling association.reader" do ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never Author.preload(:readonly_comments).first! end + + private + def find_all_ordered(klass, include = nil) + klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a + end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index f707a170f5..5eacb5a3d8 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" 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 f73005b3cb..c817d7267b 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/computer" @@ -86,12 +88,6 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end -ActiveSupport::Deprecation.silence do - class DeveloperWithConstantClassName < Developer - has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys - end -end - class DeveloperWithExtendOption < Developer module NamedExtension def category @@ -952,13 +948,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end end - def test_with_constant_class_name - assert_nothing_raised do - developer = DeveloperWithConstantClassName.new - developer.projects - end - end - def test_alternate_database professor = Professor.create(name: "Plum") course = Course.create(name: "Forensics") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a7e16af88d..18548f8516 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/computer" @@ -61,7 +63,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase assert_equal 2, subscriber.subscriptions.size end - assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: "webster132") + assert_equal Subscription.where(subscriber_id: "webster132"), subscriber.subscriptions end def test_association_primary_key_on_new_record_should_fetch_with_query @@ -72,12 +74,23 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase assert_equal 1, author.essays.size end - assert_equal author.essays, Essay.where(writer_id: "David") + assert_equal Essay.where(writer_id: "David"), author.essays end def test_has_many_custom_primary_key david = authors(:david) - assert_equal david.essays, Essay.where(writer_id: "David") + assert_equal Essay.where(writer_id: "David"), david.essays + end + + def test_ids_on_unloaded_association_with_custom_primary_key + david = people(:david) + assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids + end + + def test_ids_on_loaded_association_with_custom_primary_key + david = people(:david) + david.essays.load + assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids end def test_has_many_assignment_with_custom_primary_key @@ -463,7 +476,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_resets_cached_counters + Reader.delete_all + person = Person.create!(first_name: "tenderlove") + post = Post.first assert_equal [], person.readers @@ -497,21 +513,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_predicate person.references, :exists? end - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 3, Firm.all.merge!(order: "id").first.clients.count + assert_equal 3, Firm.first.clients.count end def test_counting - assert_equal 3, Firm.all.merge!(order: "id").first.plain_clients.count + assert_equal 3, Firm.first.plain_clients.count end def test_counting_with_single_hash - assert_equal 1, Firm.all.merge!(order: "id").first.plain_clients.where(name: "Microsoft").count + assert_equal 1, Firm.first.plain_clients.where(name: "Microsoft").count end def test_counting_with_column_name_and_hash - assert_equal 3, Firm.all.merge!(order: "id").first.plain_clients.count(:name) + assert_equal 3, Firm.first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -521,7 +536,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 3, Firm.all.merge!(order: "id").first.clients.length + assert_equal 3, Firm.first.clients.length end def test_finding_array_compatibility @@ -561,8 +576,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_taking_with_a_number + klass = Class.new(Author) do + has_many :posts, -> { order(:id) } + + def self.name + "Author" + end + end + # taking from unloaded Relation - bob = Author.find(authors(:bob).id) + bob = klass.find(authors(:bob).id) new_post = bob.posts.build assert_not bob.posts.loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) @@ -593,27 +616,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding_default_orders - assert_equal "Summit", Firm.all.merge!(order: "id").first.clients.first.name + assert_equal "Summit", Firm.first.clients.first.name end def test_finding_with_different_class_name_and_order - assert_equal "Apex", Firm.all.merge!(order: "id").first.clients_sorted_desc.first.name + assert_equal "Apex", Firm.first.clients_sorted_desc.first.name end def test_finding_with_foreign_key - assert_equal "Microsoft", Firm.all.merge!(order: "id").first.clients_of_firm.first.name + assert_equal "Microsoft", Firm.first.clients_of_firm.first.name end def test_finding_with_condition - assert_equal "Microsoft", Firm.all.merge!(order: "id").first.clients_like_ms.first.name + assert_equal "Microsoft", Firm.first.clients_like_ms.first.name end def test_finding_with_condition_hash - assert_equal "Microsoft", Firm.all.merge!(order: "id").first.clients_like_ms_with_hash_conditions.first.name + assert_equal "Microsoft", Firm.first.clients_like_ms_with_hash_conditions.first.name end def test_finding_using_primary_key - assert_equal "Summit", Firm.all.merge!(order: "id").first.clients_using_primary_key.first.name + assert_equal "Summit", Firm.first.clients_using_primary_key.first.name end def test_update_all_on_association_accessed_before_save @@ -636,7 +659,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.all.merge!(order: "id").first + firm = Firm.first assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -656,7 +679,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_one_message_on_primary_key - firm = Firm.all.merge!(order: "id").first + firm = Firm.first e = assert_raises(ActiveRecord::RecordNotFound) do firm.clients.find(0) @@ -682,7 +705,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.all.merge!(order: "id").first + firm = Firm.first assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length end @@ -727,29 +750,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all_sanitized - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.all.merge!(order: "id").first + firm = Firm.first summit = firm.clients.where("name = 'Summit'").to_a assert_equal summit, firm.clients.where("name = ?", "Summit").to_a assert_equal summit, firm.clients.where("name = :name", name: "Summit").to_a end def test_find_first - firm = Firm.all.merge!(order: "id").first + firm = Firm.first client2 = Client.find(2) assert_equal firm.clients.first, firm.clients.order("id").first assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").order("id").first end def test_find_first_sanitized - firm = Firm.all.merge!(order: "id").first + firm = Firm.first client2 = Client.find(2) - assert_equal client2, firm.clients.merge!(where: ["#{QUOTED_TYPE} = ?", "Client"], order: "id").first - assert_equal client2, firm.clients.merge!(where: ["#{QUOTED_TYPE} = :type", { type: "Client" }], order: "id").first + assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = ?", "Client").first + assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = :type", type: "Client").first end def test_find_first_after_reset_scope - firm = Firm.all.merge!(order: "id").first + firm = Firm.first collection = firm.clients original_object = collection.first @@ -760,7 +782,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_first_after_reset - firm = Firm.all.merge!(order: "id").first + firm = Firm.first collection = firm.clients original_object = collection.first @@ -772,7 +794,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_first_after_reload - firm = Firm.all.merge!(order: "id").first + firm = Firm.first collection = firm.clients original_object = collection.first @@ -809,7 +831,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped_having - assert_equal 1, authors(:david).popular_grouped_posts.length + assert_equal 2, authors(:david).popular_grouped_posts.length assert_equal 0, authors(:mary).popular_grouped_posts.length end @@ -875,7 +897,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_with_bang_on_has_many_raises_when_record_not_saved assert_raise(ActiveRecord::RecordInvalid) do - firm = Firm.all.merge!(order: "id").first + firm = Firm.first firm.plain_clients.create! end end @@ -1557,8 +1579,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_destroy_dependent_when_deleted_from_association - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.all.merge!(order: "id").first + firm = Firm.first assert_equal 3, firm.clients.size client = firm.clients.first @@ -1668,7 +1689,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_less - firm = Firm.all.merge!(order: "id").first + firm = Firm.first firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -1682,7 +1703,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.all.merge!(order: "id").first + firm = Firm.first firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -2080,7 +2101,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.all.merge!(order: "id").first + firm = Firm.first client = firm.clients_using_primary_key.create!(name: "test") assert_equal firm.name, client.firm_name end @@ -2329,7 +2350,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "does not duplicate associations when used with natural primary keys" do speedometer = Speedometer.create!(id: "4") - speedometer.minivans.create!(minivan_id: "a-van-red" , name: "a van", color: "red") + speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red") assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}" assert_equal 1, speedometer.reload.minivans.to_a.size @@ -2488,6 +2509,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_same car, new_bulb.car end + test "reattach to new objects replaces inverse association and foreign key" do + bulb = Bulb.create!(car: Car.create!) + assert bulb.car_id + car = Car.new + car.bulbs << bulb + assert_equal car, bulb.car + assert_nil bulb.car_id + end + test "in memory replacement maintains order" do first_bulb = Bulb.create! second_bulb = Bulb.create! @@ -2499,6 +2529,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + test "association size calculation works with default scoped selects when not previously fetched" do + firm = Firm.create!(name: "Firm") + 5.times { firm.developers_with_select << Developer.create!(name: "Developer") } + + same_firm = Firm.find(firm.id) + assert_equal 5, same_firm.developers_with_select.size + end + test "prevent double insertion of new object when the parent association loaded in the after save callback" do reset_callbacks(:save, Bulb) do Bulb.after_save { |record| record.car.bulbs.load } 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 1c2138a3d0..7c9c9e81ab 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/person" @@ -815,7 +817,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase sarah = Person.create!(first_name: "Sarah", primary_contact_id: people(:susan).id, gender: "F", number1_fan_id: 1) john = Person.create!(first_name: "John", primary_contact_id: sarah.id, gender: "M", number1_fan_id: 1) assert_equal sarah.agents, [john] - assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents + assert_equal people(:susan).agents.flat_map(&:agents).sort, people(:susan).agents_of_agents.sort end def test_associate_existing_with_nonstandard_primary_key_on_belongs_to @@ -867,6 +869,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [dev], company.developers end + def test_collection_singular_ids_setter_with_required_type_cast + company = companies(:rails_core) + dev = Developer.first + + company.developer_ids = [dev.id.to_s] + assert_equal [dev], company.developers + end + def test_collection_singular_ids_setter_with_string_primary_keys assert_nothing_raised do book = books(:awdr) @@ -878,32 +888,20 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_collection_singular_ids_setter_with_changed_primary_key - company = companies(:first_firm) - client = companies(:first_client) - company.clients_using_primary_key_ids = [client.name] - assert_equal [client], company.clients_using_primary_key - end - def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) ids = [Developer.first.id, -9999] e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids } - assert_match(/Couldn't find all Developers with 'id'/, e.message) - end - - def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set_with_changed_primary_key - company = companies(:first_firm) - ids = [Client.first.name, "unknown client"] - e = assert_raises(ActiveRecord::RecordNotFound) { company.clients_using_primary_key_ids = ids } - assert_match(/Couldn't find all Clients with 'name'/, e.message) + msg = "Couldn't find all Developers with 'id': (1, -9999) (found 1 results, but was looking for 2). Couldn't find Developer with id -9999." + assert_equal(msg, e.message) end def test_collection_singular_ids_through_setter_raises_exception_when_invalid_ids_set author = authors(:david) ids = [categories(:general).name, "Unknown"] e = assert_raises(ActiveRecord::RecordNotFound) { author.essay_category_ids = ids } - assert_equal "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2)", e.message + msg = "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2). Couldn't find Category with name Unknown." + assert_equal msg, e.message end def test_build_a_model_from_hm_through_association_with_where_clause @@ -946,6 +944,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_many_through_polymorphic_with_rewhere + post = TaggedPost.create!(title: "Tagged", body: "Post") + tag = post.tags.create!(name: "Tag") + assert_equal [tag], TaggedPost.preload(:tags).last.tags + assert_equal [tag], TaggedPost.eager_load(:tags).last.tags + end + def test_has_many_through_polymorphic_with_primary_key_option assert_equal [categories(:general)], authors(:david).essay_categories @@ -1128,6 +1133,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } end + def test_has_many_through_associations_sum_on_columns + post1 = Post.create(title: "active", body: "sample") + post2 = Post.create(title: "inactive", body: "sample") + + person1 = Person.create(first_name: "aaron", followers_count: 1) + person2 = Person.create(first_name: "schmit", followers_count: 2) + person3 = Person.create(first_name: "bill", followers_count: 3) + person4 = Person.create(first_name: "cal", followers_count: 4) + + Reader.create(post_id: post1.id, person_id: person1.id) + Reader.create(post_id: post1.id, person_id: person2.id) + Reader.create(post_id: post1.id, person_id: person3.id) + Reader.create(post_id: post1.id, person_id: person4.id) + + Reader.create(post_id: post2.id, person_id: person1.id) + Reader.create(post_id: post2.id, person_id: person2.id) + Reader.create(post_id: post2.id, person_id: person3.id) + Reader.create(post_id: post2.id, person_id: person4.id) + + active_persons = Person.joins(:readers).joins(:posts).distinct(true).where("posts.title" => "active") + + assert_equal active_persons.map(&:followers_count).reduce(:+), 10 + assert_equal active_persons.sum(:followers_count), 10 + assert_equal active_persons.sum(:followers_count), active_persons.map(&:followers_count).reduce(:+) + end + def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new @@ -1227,6 +1258,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase TenantMembership.current_member = nil end + def test_has_many_through_with_scope_that_has_joined_same_table_with_parent_relation + assert_equal authors(:david), Author.joins(:comments_for_first_author).take + end + + def test_has_many_through_with_unscope_should_affect_to_through_scope + assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments + end + + def test_has_many_through_with_scope_should_accept_string_and_hash_join + assert_equal authors(:david), Author.joins({ comments_for_first_author: :post }, "inner join posts posts_alias on authors.id = posts_alias.author_id").eager_load(:categories).take + end + def test_has_many_through_with_scope_should_respect_table_alias family = Family.create! users = 3.times.map { User.create! } @@ -1238,6 +1281,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 0, users[2].family_members.to_a.size end + def test_through_scope_is_affected_by_unscoping + author = authors(:david) + + expected = author.comments.to_a + FirstPost.unscoped do + assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id) + end + end + + def test_through_scope_isnt_affected_by_scoping + author = authors(:david) + + expected = author.comments_on_first_posts.to_a + FirstPost.where(id: 2).scoping do + author.comments_on_first_posts.reset + assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id) + end + end + def test_incorrectly_ordered_through_associations assert_raises(ActiveRecord::HasManyThroughOrderError) do DeveloperWithIncorrectlyOrderedHasManyThrough.create( diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index bf3b8dcd63..ec5d95080b 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/computer" @@ -13,7 +15,7 @@ require "models/post" class HasOneAssociationsTest < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? - fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates + fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates, :authors, :author_addresses def setup Account.destroyed_account_ids.clear @@ -28,7 +30,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase ActiveRecord::SQLCounter.clear_log companies(:first_firm).account ensure - assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query" + log_all = ActiveRecord::SQLCounter.log_all + assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}" end def test_has_one_cache_nils @@ -688,4 +691,38 @@ class HasOneAssociationsTest < ActiveRecord::TestCase SpecialAuthor.joins(book: :subscription).where.not(where_clause) end end + + class DestroyByParentBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyByParentAuthor" + before_destroy :dont, unless: :destroyed_by_association + + def dont + throw(:abort) + end + end + + class DestroyByParentAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "DestroyByParentBook", foreign_key: "author_id", dependent: :destroy + end + + test "destroyed_by_association set in child destroy callback on parent destroy" do + author = DestroyByParentAuthor.create!(name: "Test") + book = DestroyByParentBook.create!(author: author) + + author.destroy + + assert_not DestroyByParentBook.exists?(book.id) + end + + test "destroyed_by_association set in child destroy callback on replace" do + author = DestroyByParentAuthor.create!(name: "Test") + book = DestroyByParentBook.create!(author: author) + + author.book = DestroyByParentBook.create! + author.save! + + assert_not DestroyByParentBook.exists?(book.id) + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 28b883586d..1d37457464 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/club" require "models/member_type" @@ -98,7 +100,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_eager_loading - members = assert_queries(3) do #base table, through table, clubs table + members = assert_queries(3) do # base table, through table, clubs table Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size @@ -106,7 +108,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_eager_loading_through_polymorphic - members = assert_queries(3) do #base table, through table, clubs table + members = assert_queries(3) do # base table, through table, clubs table Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size @@ -137,7 +139,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eagerloading members = assert_queries(1) do - Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback + Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].club } @@ -145,7 +147,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic members = assert_queries(1) do - Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback + Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -154,7 +156,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save! members = assert_queries(1) do - Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a #force fallback + Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index ddf5bc6f0b..7be875fec6 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -25,6 +27,24 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end end + def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins + sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql + assert_match(/agents_people_4/i, sql) + end + + def test_construct_finder_sql_does_not_table_name_collide_with_string_joins + sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql + assert_match(/agents_people_2/i, sql) + end + + def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins + people = Person.arel_table + agents = people.alias("agents_people") + constraint = agents[:primary_contact_id].eq(people[:id]) + sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql + assert_match(/agents_people_2/i, sql) + end + def test_construct_finder_sql_ignores_empty_joins_hash sql = Author.joins({}).to_sql assert_no_match(/JOIN/i, sql) diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 9fbaa43274..c0d328ca8a 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/man" require "models/face" @@ -16,6 +18,8 @@ require "models/admin/user" require "models/developer" require "models/company" require "models/project" +require "models/author" +require "models/post" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -58,6 +62,20 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection" end + def test_has_many_and_belongs_to_should_find_inverse_automatically_for_sti + author_reflection = Author.reflect_on_association(:posts) + author_child_reflection = Author.reflect_on_association(:special_posts) + post_reflection = Post.reflect_on_association(:author) + + assert_respond_to author_reflection, :has_inverse? + assert author_reflection.has_inverse?, "The Author reflection should have an inverse" + assert_equal post_reflection, author_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection" + + assert_respond_to author_child_reflection, :has_inverse? + assert author_child_reflection.has_inverse?, "The Author reflection should have an inverse" + assert_equal post_reflection, author_child_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection" + end + def test_has_one_and_belongs_to_automatic_inverse_shares_objects car = Car.first bulb = Bulb.create!(car: car) @@ -265,7 +283,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end class InverseHasManyTests < ActiveRecord::TestCase - fixtures :men, :interests + fixtures :men, :interests, :posts, :authors, :author_addresses def test_parent_instance_should_be_shared_with_every_child_on_find m = men(:gordon) @@ -279,6 +297,27 @@ class InverseHasManyTests < ActiveRecord::TestCase end end + def test_parent_instance_should_be_shared_with_every_child_on_find_for_sti + a = authors(:david) + ps = a.posts + ps.each do |p| + assert_equal a.name, p.author.name, "Name of man should be the same before changes to parent instance" + a.name = "Bongo" + assert_equal a.name, p.author.name, "Name of man should be the same after changes to parent instance" + p.author.name = "Mungo" + assert_equal a.name, p.author.name, "Name of man should be the same after changes to child-owned instance" + end + + sps = a.special_posts + sps.each do |sp| + assert_equal a.name, sp.author.name, "Name of man should be the same before changes to parent instance" + a.name = "Bongo" + assert_equal a.name, sp.author.name, "Name of man should be the same after changes to parent instance" + sp.author.name = "Mungo" + assert_equal a.name, sp.author.name, "Name of man should be the same after changes to child-owned instance" + end + end + def test_parent_instance_should_be_shared_with_eager_loaded_children m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests).first is = m.interests @@ -445,7 +484,10 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_raise_record_not_found_error_when_no_ids_are_passed man = Man.create! - assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() } + exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() } + + assert_equal exception.model, "Interest" + assert_equal exception.primary_key, "id" end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error @@ -633,6 +675,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal old_inversed_man.object_id, new_inversed_man.object_id end + def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation + face = Face.new man: Man.new + + old_inversed_man = face.man + face.save! + new_inversed_man = face.man + + assert_equal old_inversed_man.object_id, new_inversed_man.object_id + end + def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many i = interests(:llama_wrangling) m = i.polymorphic_man diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index c078cef064..5d83c9435b 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/tag" require "models/tagging" @@ -97,11 +99,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class - post = SubStiPost.create title: "SubStiPost", body: "SubStiPost body" - assert_instance_of SubStiPost, post + post = SubAbstractStiPost.create title: "SubAbstractStiPost", body: "SubAbstractStiPost body" + assert_instance_of SubAbstractStiPost, post tagging = tags(:misc).taggings.create(taggable: post) - assert_equal "SubStiPost", tagging.taggable_type + assert_equal "SubAbstractStiPost", tagging.taggable_type end def test_polymorphic_has_many_going_through_join_model_with_inheritance @@ -402,7 +404,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2 + assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2.sort_by(&:id) end def test_has_many_through_polymorphic_has_many @@ -465,7 +467,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_tag = Tag.new(name: "new") saved_post.tags << new_tag - assert new_tag.persisted? #consistent with habtm! + assert new_tag.persisted? # consistent with habtm! assert saved_post.persisted? assert_includes saved_post.tags, new_tag @@ -494,25 +496,25 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase post_thinking = posts(:thinking) assert_nothing_raised { post_thinking.tags << push } assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, - message = "Expected a Tag in tags collection, got #{wrong.class}.") + "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, - message = "Expected a Tagging in taggings collection, got #{wrong.class}.") + "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags.reload.size) assert_kind_of Tag, post_thinking.tags.create!(name: "foo") assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, - message = "Expected a Tag in tags collection, got #{wrong.class}.") + "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, - message = "Expected a Tagging in taggings collection, got #{wrong.class}.") + "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags.reload.size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(name: "abc"), Tag.create!(name: "def")) } assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, - message = "Expected a Tag in tags collection, got #{wrong.class}.") + "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, - message = "Expected a Tagging in taggings collection, got #{wrong.class}.") + "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags.reload.size) 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 6d3757f467..c95d0425cd 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 67ff7355b3..65d30d011b 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/post" @@ -22,6 +24,11 @@ require "models/category" require "models/categorization" require "models/membership" require "models/essay" +require "models/hotel" +require "models/department" +require "models/chef" +require "models/cake_designer" +require "models/drink_designer" class NestedThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, @@ -423,6 +430,11 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert authors.empty? end + def test_nested_has_many_through_with_scope_on_polymorphic_reflection + authors = Author.joins(:ordered_posts).where("posts.id" => posts(:misc_by_bob).id) + assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id) + end + def test_has_many_through_with_foreign_key_option_on_through_reflection assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order("posts.id") assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors @@ -567,6 +579,37 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert !c.post_taggings.empty? end + def test_polymorphic_has_many_through_when_through_association_has_not_loaded + cake_designer = CakeDesigner.create!(chef: Chef.new) + drink_designer = DrinkDesigner.create!(chef: Chef.new) + department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef]) + Hotel.create!(departments: [department]) + hotel = Hotel.includes(:cake_designers, :drink_designers).take + + assert_equal [cake_designer], hotel.cake_designers + assert_equal [drink_designer], hotel.drink_designers + end + + def test_polymorphic_has_many_through_when_through_association_has_already_loaded + cake_designer = CakeDesigner.create!(chef: Chef.new) + drink_designer = DrinkDesigner.create!(chef: Chef.new) + department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef]) + Hotel.create!(departments: [department]) + hotel = Hotel.includes(:chefs, :cake_designers, :drink_designers).take + + assert_equal [cake_designer], hotel.cake_designers + assert_equal [drink_designer], hotel.drink_designers + end + + def test_polymorphic_has_many_through_joined_different_table_twice + cake_designer = CakeDesigner.create!(chef: Chef.new) + drink_designer = DrinkDesigner.create!(chef: Chef.new) + department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef]) + hotel = Hotel.create!(departments: [department]) + + assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take + end + private def assert_includes_and_joins_equal(query, expected, association) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 45e1803858..65a3bb5efe 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class RequiredAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 2eb31326a5..de04221ea6 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/computer" require "models/developer" @@ -245,16 +247,16 @@ class AssociationProxyTest < ActiveRecord::TestCase test "first! works on loaded associations" do david = authors(:david) - assert_equal david.posts.first, david.posts.reload.first! - assert david.posts.loaded? - assert_no_queries { david.posts.first! } + assert_equal david.first_posts.first, david.first_posts.reload.first! + assert david.first_posts.loaded? + assert_no_queries { david.first_posts.first! } end def test_pluck_uses_loaded_target david = authors(:david) - assert_equal david.posts.pluck(:title), david.posts.load.pluck(:title) - assert david.posts.loaded? - assert_no_queries { david.posts.pluck(:title) } + assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title) + assert david.first_posts.loaded? + assert_no_queries { david.first_posts.pluck(:title) } end def test_reset_unloads_target diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index cfa6ed1da6..42eca233ce 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 1fc63a49d4..0170a6e98d 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -8,11 +10,10 @@ module ActiveRecord end def setup - @klass = Class.new do + @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.decorate_matching_attribute_types(*); end - def self.initialize_generated_modules; end include ActiveRecord::DefineCallbacks include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 4d24a980dc..c48f7d3518 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/minimalistic" require "models/developer" @@ -198,12 +200,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:Mysql2Adapter) test "read attributes_before_type_cast on a boolean" do bool = Boolean.create!("value" => false) - if RUBY_PLATFORM.include?("java") - # JRuby will return the value before typecast as string. - assert_equal "0", bool.reload.attributes_before_type_cast["value"] - else - assert_equal 0, bool.reload.attributes_before_type_cast["value"] - end + assert_equal 0, bool.reload.attributes_before_type_cast["value"] end end @@ -997,6 +994,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal ["title"], model.accessed_fields end + test "generated attribute methods ancestors have correct class" do + mod = Topic.send(:generated_attribute_methods) + assert_match %r(GeneratedAttributeMethods), mod.inspect + end + private def new_topic_like_ar_class(&block) @@ -1005,7 +1007,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase class_eval(&block) end - assert_empty klass.generated_attribute_methods.instance_methods(false) + assert_empty klass.send(:generated_attribute_methods).instance_methods(false) klass end @@ -1017,14 +1019,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.time_zone_aware_types = old_types end - def cached_columns - Topic.columns.map(&:name) - end - - def time_related_columns_on_topic - Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) } - end - def privatize(method_signature) @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1) private diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb deleted file mode 100644 index bd4b200735..0000000000 --- a/activerecord/test/cases/attribute_set_test.rb +++ /dev/null @@ -1,253 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class AttributeSetTest < ActiveRecord::TestCase - test "building a new set from raw attributes" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - attributes = builder.build_from_database(foo: "1.1", bar: "2.2") - - assert_equal 1, attributes[:foo].value - assert_equal 2.2, attributes[:bar].value - assert_equal :foo, attributes[:foo].name - assert_equal :bar, attributes[:bar].name - end - - test "building with custom types" do - builder = AttributeSet::Builder.new(foo: Type::Float.new) - attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, bar: Type::Integer.new) - - assert_equal 3.3, attributes[:foo].value - assert_equal 4, attributes[:bar].value - end - - test "[] returns a null object" do - builder = AttributeSet::Builder.new(foo: Type::Float.new) - attributes = builder.build_from_database(foo: "3.3") - - assert_equal "3.3", attributes[:foo].value_before_type_cast - assert_nil attributes[:bar].value_before_type_cast - assert_equal :bar, attributes[:bar].name - end - - test "duping creates a new hash, but does not dup the attributes" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) - attributes = builder.build_from_database(foo: 1, bar: "foo") - - # Ensure the type cast value is cached - attributes[:foo].value - attributes[:bar].value - - duped = attributes.dup - duped.write_from_database(:foo, 2) - duped[:bar].value << "bar" - - assert_equal 1, attributes[:foo].value - assert_equal 2, duped[:foo].value - assert_equal "foobar", attributes[:bar].value - assert_equal "foobar", duped[:bar].value - end - - test "deep_duping creates a new hash and dups each attribute" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) - attributes = builder.build_from_database(foo: 1, bar: "foo") - - # Ensure the type cast value is cached - attributes[:foo].value - attributes[:bar].value - - duped = attributes.deep_dup - duped.write_from_database(:foo, 2) - duped[:bar].value << "bar" - - assert_equal 1, attributes[:foo].value - assert_equal 2, duped[:foo].value - assert_equal "foo", attributes[:bar].value - assert_equal "foobar", duped[:bar].value - end - - test "freezing cloned set does not freeze original" do - attributes = AttributeSet.new({}) - clone = attributes.clone - - clone.freeze - - assert clone.frozen? - assert_not attributes.frozen? - end - - test "to_hash returns a hash of the type cast values" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - attributes = builder.build_from_database(foo: "1.1", bar: "2.2") - - assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) - assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) - end - - test "to_hash maintains order" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - attributes = builder.build_from_database(foo: "2.2", bar: "3.3") - - attributes[:bar] - hash = attributes.to_h - - assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a - end - - test "values_before_type_cast" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) - attributes = builder.build_from_database(foo: "1.1", bar: "2.2") - - assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast) - end - - test "known columns are built with uninitialized attributes" do - attributes = attributes_with_uninitialized_key - assert attributes[:foo].initialized? - assert_not attributes[:bar].initialized? - end - - test "uninitialized attributes are not included in the attributes hash" do - attributes = attributes_with_uninitialized_key - assert_equal({ foo: 1 }, attributes.to_hash) - end - - test "uninitialized attributes are not included in keys" do - attributes = attributes_with_uninitialized_key - assert_equal [:foo], attributes.keys - end - - test "uninitialized attributes return false for key?" do - attributes = attributes_with_uninitialized_key - assert attributes.key?(:foo) - assert_not attributes.key?(:bar) - end - - test "unknown attributes return false for key?" do - attributes = attributes_with_uninitialized_key - assert_not attributes.key?(:wibble) - end - - test "fetch_value returns the value for the given initialized attribute" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - attributes = builder.build_from_database(foo: "1.1", bar: "2.2") - - assert_equal 1, attributes.fetch_value(:foo) - assert_equal 2.2, attributes.fetch_value(:bar) - end - - test "fetch_value returns nil for unknown attributes" do - attributes = attributes_with_uninitialized_key - assert_nil attributes.fetch_value(:wibble) { "hello" } - end - - test "fetch_value returns nil for unknown attributes when types has a default" do - types = Hash.new(Type::Value.new) - builder = AttributeSet::Builder.new(types) - attributes = builder.build_from_database - - assert_nil attributes.fetch_value(:wibble) { "hello" } - end - - test "fetch_value uses the given block for uninitialized attributes" do - attributes = attributes_with_uninitialized_key - value = attributes.fetch_value(:bar) { |n| n.to_s + "!" } - assert_equal "bar!", value - end - - test "fetch_value returns nil for uninitialized attributes if no block is given" do - attributes = attributes_with_uninitialized_key - assert_nil attributes.fetch_value(:bar) - end - - test "the primary_key is always initialized" do - builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo) - attributes = builder.build_from_database - - assert attributes.key?(:foo) - assert_equal [:foo], attributes.keys - assert attributes[:foo].initialized? - end - - class MyType - def cast(value) - return if value.nil? - value + " from user" - end - - def deserialize(value) - return if value.nil? - value + " from database" - end - - def assert_valid_value(*) - end - end - - test "write_from_database sets the attribute with database typecasting" do - builder = AttributeSet::Builder.new(foo: MyType.new) - attributes = builder.build_from_database - - assert_nil attributes.fetch_value(:foo) - - attributes.write_from_database(:foo, "value") - - assert_equal "value from database", attributes.fetch_value(:foo) - end - - test "write_from_user sets the attribute with user typecasting" do - builder = AttributeSet::Builder.new(foo: MyType.new) - attributes = builder.build_from_database - - assert_nil attributes.fetch_value(:foo) - - attributes.write_from_user(:foo, "value") - - assert_equal "value from user", attributes.fetch_value(:foo) - end - - def attributes_with_uninitialized_key - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - builder.build_from_database(foo: "1.1") - end - - test "freezing doesn't prevent the set from materializing" do - builder = AttributeSet::Builder.new(foo: Type::String.new) - attributes = builder.build_from_database(foo: "1") - - attributes.freeze - assert_equal({ foo: "1" }, attributes.to_hash) - end - - test "#accessed_attributes returns only attributes which have been read" do - builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new) - attributes = builder.build_from_database(foo: "1", bar: "2") - - assert_equal [], attributes.accessed - - attributes.fetch_value(:foo) - - assert_equal [:foo], attributes.accessed - end - - test "#map returns a new attribute set with the changes applied" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) - attributes = builder.build_from_database(foo: "1", bar: "2") - new_attributes = attributes.map do |attr| - attr.with_cast_value(attr.value + 1) - end - - assert_equal 2, new_attributes.fetch_value(:foo) - assert_equal 3, new_attributes.fetch_value(:bar) - end - - test "comparison for equality is correctly implemented" do - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) - attributes = builder.build_from_database(foo: "1", bar: "2") - attributes2 = builder.build_from_database(foo: "1", bar: "2") - attributes3 = builder.build_from_database(foo: "2", bar: "2") - - assert_equal attributes, attributes2 - assert_not_equal attributes2, attributes3 - end - end -end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb deleted file mode 100644 index e856d551c0..0000000000 --- a/activerecord/test/cases/attribute_test.rb +++ /dev/null @@ -1,253 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class AttributeTest < ActiveRecord::TestCase - setup do - @type = Minitest::Mock.new - end - - teardown do - assert @type.verify - end - - test "from_database + read type casts from database" do - @type.expect(:deserialize, "type cast from database", ["a value"]) - attribute = Attribute.from_database(nil, "a value", @type) - - type_cast_value = attribute.value - - assert_equal "type cast from database", type_cast_value - end - - test "from_user + read type casts from user" do - @type.expect(:cast, "type cast from user", ["a value"]) - attribute = Attribute.from_user(nil, "a value", @type) - - type_cast_value = attribute.value - - assert_equal "type cast from user", type_cast_value - end - - test "reading memoizes the value" do - @type.expect(:deserialize, "from the database", ["whatever"]) - attribute = Attribute.from_database(nil, "whatever", @type) - - type_cast_value = attribute.value - second_read = attribute.value - - assert_equal "from the database", type_cast_value - assert_same type_cast_value, second_read - end - - test "reading memoizes falsy values" do - @type.expect(:deserialize, false, ["whatever"]) - attribute = Attribute.from_database(nil, "whatever", @type) - - attribute.value - attribute.value - end - - test "read_before_typecast returns the given value" do - attribute = Attribute.from_database(nil, "raw value", @type) - - raw_value = attribute.value_before_type_cast - - assert_equal "raw value", raw_value - end - - test "from_database + read_for_database type casts to and from database" do - @type.expect(:deserialize, "read from database", ["whatever"]) - @type.expect(:serialize, "ready for database", ["read from database"]) - attribute = Attribute.from_database(nil, "whatever", @type) - - serialize = attribute.value_for_database - - assert_equal "ready for database", serialize - end - - test "from_user + read_for_database type casts from the user to the database" do - @type.expect(:cast, "read from user", ["whatever"]) - @type.expect(:serialize, "ready for database", ["read from user"]) - attribute = Attribute.from_user(nil, "whatever", @type) - - serialize = attribute.value_for_database - - assert_equal "ready for database", serialize - end - - test "duping dups the value" do - @type.expect(:deserialize, "type cast".dup, ["a value"]) - attribute = Attribute.from_database(nil, "a value", @type) - - value_from_orig = attribute.value - value_from_clone = attribute.dup.value - value_from_orig << " foo" - - assert_equal "type cast foo", value_from_orig - assert_equal "type cast", value_from_clone - end - - test "duping does not dup the value if it is not dupable" do - @type.expect(:deserialize, false, ["a value"]) - attribute = Attribute.from_database(nil, "a value", @type) - - assert_same attribute.value, attribute.dup.value - end - - test "duping does not eagerly type cast if we have not yet type cast" do - attribute = Attribute.from_database(nil, "a value", @type) - attribute.dup - end - - class MyType - def cast(value) - value + " from user" - end - - def deserialize(value) - value + " from database" - end - - def assert_valid_value(*) - end - end - - test "with_value_from_user returns a new attribute with the value from the user" do - old = Attribute.from_database(nil, "old", MyType.new) - new = old.with_value_from_user("new") - - assert_equal "old from database", old.value - assert_equal "new from user", new.value - end - - test "with_value_from_database returns a new attribute with the value from the database" do - old = Attribute.from_user(nil, "old", MyType.new) - new = old.with_value_from_database("new") - - assert_equal "old from user", old.value - assert_equal "new from database", new.value - end - - test "uninitialized attributes yield their name if a block is given to value" do - block = proc { |name| name.to_s + "!" } - foo = Attribute.uninitialized(:foo, nil) - bar = Attribute.uninitialized(:bar, nil) - - assert_equal "foo!", foo.value(&block) - assert_equal "bar!", bar.value(&block) - end - - test "uninitialized attributes have no value" do - assert_nil Attribute.uninitialized(:foo, nil).value - end - - test "attributes equal other attributes with the same constructor arguments" do - first = Attribute.from_database(:foo, 1, Type::Integer.new) - second = Attribute.from_database(:foo, 1, Type::Integer.new) - assert_equal first, second - end - - test "attributes do not equal attributes with different names" do - first = Attribute.from_database(:foo, 1, Type::Integer.new) - second = Attribute.from_database(:bar, 1, Type::Integer.new) - assert_not_equal first, second - end - - test "attributes do not equal attributes with different types" do - first = Attribute.from_database(:foo, 1, Type::Integer.new) - second = Attribute.from_database(:foo, 1, Type::Float.new) - assert_not_equal first, second - end - - test "attributes do not equal attributes with different values" do - first = Attribute.from_database(:foo, 1, Type::Integer.new) - second = Attribute.from_database(:foo, 2, Type::Integer.new) - assert_not_equal first, second - end - - test "attributes do not equal attributes of other classes" do - first = Attribute.from_database(:foo, 1, Type::Integer.new) - second = Attribute.from_user(:foo, 1, Type::Integer.new) - assert_not_equal first, second - end - - test "an attribute has not been read by default" do - attribute = Attribute.from_database(:foo, 1, Type::Value.new) - assert_not attribute.has_been_read? - end - - test "an attribute has been read when its value is calculated" do - attribute = Attribute.from_database(:foo, 1, Type::Value.new) - attribute.value - assert attribute.has_been_read? - end - - test "an attribute is not changed if it hasn't been assigned or mutated" do - attribute = Attribute.from_database(:foo, 1, Type::Value.new) - - refute attribute.changed? - end - - test "an attribute is changed if it's been assigned a new value" do - attribute = Attribute.from_database(:foo, 1, Type::Value.new) - changed = attribute.with_value_from_user(2) - - assert changed.changed? - end - - test "an attribute is not changed if it's assigned the same value" do - attribute = Attribute.from_database(:foo, 1, Type::Value.new) - unchanged = attribute.with_value_from_user(1) - - refute unchanged.changed? - end - - test "an attribute can not be mutated if it has not been read, - and skips expensive calculations" do - type_which_raises_from_all_methods = Object.new - attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) - - assert_not attribute.changed_in_place? - end - - test "an attribute is changed if it has been mutated" do - attribute = Attribute.from_database(:foo, "bar", Type::String.new) - attribute.value << "!" - - assert attribute.changed_in_place? - assert attribute.changed? - end - - test "an attribute can forget its changes" do - attribute = Attribute.from_database(:foo, "bar", Type::String.new) - changed = attribute.with_value_from_user("foo") - forgotten = changed.forgetting_assignment - - assert changed.changed? # sanity check - refute forgotten.changed? - end - - test "with_value_from_user validates the value" do - type = Type::Value.new - type.define_singleton_method(:assert_valid_value) do |value| - if value == 1 - raise ArgumentError - end - end - - attribute = Attribute.from_database(:foo, 1, type) - assert_equal 1, attribute.value - assert_equal 2, attribute.with_value_from_user(2).value - assert_raises ArgumentError do - attribute.with_value_from_user(1) - end - end - - test "with_type preserves mutations" do - attribute = Attribute.from_database(:foo, "".dup, Type::Value.new) - attribute.value << "1" - - assert_equal 1, attribute.with_type(Type::Integer.new).value - end - end -end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 3705a6be89..8ebfee61ff 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class OverloadedType < ActiveRecord::Base @@ -57,7 +59,7 @@ module ActiveRecord test "nonexistent attribute" do data = OverloadedType.new(non_existent_decimal: 1) - assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_equal BigDecimal(1), data.non_existent_decimal assert_raise ActiveRecord::UnknownAttributeError do UnoverloadedType.new(non_existent_decimal: 1) end @@ -106,12 +108,14 @@ module ActiveRecord assert_equal 6, klass.attribute_types.length assert_equal 6, klass.column_defaults.length + assert_equal 6, klass.attribute_names.length assert_not klass.attribute_types.include?("wibble") klass.attribute :wibble, Type::Value.new assert_equal 7, klass.attribute_types.length assert_equal 7, klass.column_defaults.length + assert_equal 7, klass.attribute_names.length assert_includes klass.attribute_types, "wibble" end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 2203aa1788..4d8368fd8a 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + require "cases/helper" require "models/bird" require "models/post" require "models/comment" require "models/company" +require "models/contract" require "models/customer" require "models/developer" require "models/computer" @@ -10,9 +13,7 @@ require "models/invoice" require "models/line_item" require "models/order" require "models/parrot" -require "models/person" require "models/pirate" -require "models/reader" require "models/ship" require "models/ship_part" require "models/tag" @@ -494,7 +495,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib end class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase - fixtures :companies, :people + fixtures :companies, :developers def test_invalid_adding firm = Firm.find(1) @@ -589,12 +590,12 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa end def test_assign_ids_for_through_a_belongs_to - post = Post.new(title: "Assigning IDs works!", body: "You heard it here first, folks!") - post.person_ids = [people(:david).id, people(:michael).id] - post.save - post.reload - assert_equal 2, post.people.length - assert_includes post.people, people(:david) + firm = Firm.new("name" => "Apple") + firm.developer_ids = [developers(:david).id, developers(:jamis).id] + firm.save + firm.reload + assert_equal 2, firm.developers.length + assert_includes firm.developers, developers(:david) end def test_build_before_save @@ -1370,7 +1371,7 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] } @pirate.save - assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) + assert_equal new_names.sort, @pirate.reload.send(@association_name).map(&:name).sort end def test_should_automatically_save_bang_the_associated_models @@ -1378,7 +1379,7 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] } @pirate.save! - assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) + assert_equal new_names.sort, @pirate.reload.send(@association_name).map(&:name).sort end def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not @@ -1722,6 +1723,10 @@ class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::Te end end + def setup + Comment.delete_all + end + def test_after_save_callback_with_autosave post = Post.new(title: "Test", body: "...") comment = post.comments.build(body: "...") diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dc32e995a4..a49990008c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/author" @@ -102,7 +104,7 @@ class BasicsTest < ActiveRecord::TestCase pk = Author.columns_hash["id"] ref = Post.columns_hash["author_id"] - assert_equal pk.bigint?, ref.bigint? + assert_equal pk.sql_type, ref.sql_type end def test_many_mutations @@ -627,7 +629,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_readonly_attributes - assert_equal Set.new([ "title" , "comments_count" ]), ReadonlyTitlePost.readonly_attributes + assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable") post.reload @@ -883,10 +885,15 @@ class BasicsTest < ActiveRecord::TestCase def test_bignum company = Company.find(1) - company.rating = 2147483647 + company.rating = 2147483648 company.save company = Company.find(1) - assert_equal 2147483647, company.rating + assert_equal 2147483648, company.rating + end + + def test_bignum_pk + company = Company.create!(id: 2147483648, name: "foo") + assert_equal company, Company.find(company.id) end # TODO: extend defaults tests to other databases! @@ -1052,29 +1059,15 @@ class BasicsTest < ActiveRecord::TestCase def test_count_with_join res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" - res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count assert_equal res, res2 - res3 = nil - assert_nothing_raised do - res3 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count - end - assert_equal res, res3 - res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" - res5 = nil - assert_nothing_raised do - res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count - end - + res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count assert_equal res4, res5 res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" - res7 = nil - assert_nothing_raised do - res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count - end + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count assert_equal res6, res7 end @@ -1449,17 +1442,81 @@ class BasicsTest < ActiveRecord::TestCase cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name) assert_includes cache_columns.keys, "first_name" assert_not_includes Developer.columns_hash.keys, "first_name" + assert_not_includes SubDeveloper.columns_hash.keys, "first_name" + assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name" 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?) 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?) + end + + test "ignored columns are stored as an array of string" do + assert_equal(%w(first_name last_name), Developer.ignored_columns) + assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns) + end + + 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=) + + developer.reload + + refute developer.respond_to?(:first_name) + refute developer.respond_to?(:first_name=) + end + + test "ignored columns not included in SELECT" do + query = Developer.all.to_sql.downcase + + # ignored column + refute 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 + query = Developer.from("developers").to_sql + quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" + + assert_match(/SELECT #{quoted_id}.* FROM developers/, query) + end + + test "using table name qualified column names unless having SELECT list explicitly" do + assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take + end + + test "protected environments by default is an array with production" do + assert_equal ["production"], ActiveRecord::Base.protected_environments + end + + def test_protected_environments_are_stored_as_an_array_of_string + previous_protected_environments = ActiveRecord::Base.protected_environments + ActiveRecord::Base.protected_environments = [:staging, "production"] + assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments + ensure + ActiveRecord::Base.protected_environments = previous_protected_environments end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 1a66b82b2e..be8aeed5ac 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + require "cases/helper" +require "models/comment" require "models/post" require "models/subscriber" @@ -583,31 +586,74 @@ class EachTest < ActiveRecord::TestCase end end - test ".error_on_ignored_order_or_limit= is deprecated" do - begin - prev = ActiveRecord::Base.error_on_ignored_order - assert_deprecated "Please use error_on_ignored_order= instead." do - ActiveRecord::Base.error_on_ignored_order_or_limit = true + test ".find_each respects table alias" do + assert_queries(1) do + table_alias = Post.arel_table.alias("omg_posts") + 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.find_each {} + end + end + + test ".find_each bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_each {} + Post.find_each {} end - assert ActiveRecord::Base.error_on_ignored_order - ensure - ActiveRecord::Base.error_on_ignored_order = prev end end - test ".error_on_ignored_order_or_limit is deprecated" do - expected = ActiveRecord::Base.error_on_ignored_order - actual = assert_deprecated "Please use error_on_ignored_order instead." do - ActiveRecord::Base.error_on_ignored_order_or_limit + test ".find_each does not disable the query cache inside the given block" do + Post.cache do + Post.find_each(start: 1, finish: 1) do |post| + assert_queries(1) do + post.comments.count + post.comments.count + end + end + end + end + + test ".find_in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_in_batches {} + Post.find_in_batches {} + end end - assert_equal expected, actual end - test "#error_on_ignored_order_or_limit is deprecated" do - expected = ActiveRecord::Base.error_on_ignored_order - actual = assert_deprecated "Please use error_on_ignored_order instead." do - Post.new.error_on_ignored_order_or_limit + test ".find_in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.find_in_batches(start: 1, finish: 1) do |batch| + assert_queries(1) do + batch.first.comments.count + batch.first.comments.count + end + end + end + end + + test ".in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.in_batches {} + Post.in_batches {} + end + end + end + + test ".in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.in_batches(start: 1, finish: 1) do |relation| + assert_queries(1) do + relation.count + relation.count + end + end end - assert_equal expected, actual end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index f6ac7990d1..d5376ece69 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" # Without using prepared statements, it makes no sense to test diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 5af44c27eb..91cc49385c 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/author" @@ -39,8 +41,9 @@ if ActiveRecord::Base.connection.prepared_statements end def test_binds_are_logged - binds = [bind_attribute("id", 1)] - sql = "select * from topics where id = #{bind_param.to_sql}" + sub = Arel::Nodes::BindParam.new(1) + binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] + sql = "select * from topics where id = #{sub.to_sql}" @connection.exec_query(sql, "SQL", binds) @@ -55,7 +58,7 @@ if ActiveRecord::Base.connection.prepared_statements end def test_logs_binds_after_type_cast - binds = [bind_attribute("id", "10", Type::Integer.new)] + binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] assert_logs_binds(binds) end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 7b8264e6e8..8f2f2c6186 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 80baaac30a..e682da6fed 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/book" require "models/club" @@ -234,6 +236,40 @@ class CalculationsTest < ActiveRecord::TestCase end 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) } + assert_queries(1) { assert_equal 3, accounts.load.size } + end + + def test_distinct_count_with_order_and_limit + assert_equal 4, Account.distinct.order(:firm_id).limit(4).count + end + + def test_distinct_count_with_order_and_offset + assert_equal 4, Account.distinct.order(:firm_id).offset(2).count + end + + def test_distinct_count_with_order_and_limit_and_offset + assert_equal 4, Account.distinct.order(:firm_id).limit(4).offset(2).count + end + + def test_distinct_joins_count_with_order_and_limit + assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).count + end + + def test_distinct_joins_count_with_order_and_offset + assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).offset(2).count + end + + def test_distinct_joins_count_with_order_and_limit_and_offset + assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count + end + + def test_distinct_count_with_group_by_and_order_and_limit + assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count) + end + def test_should_group_by_summed_field_having_condition c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit) assert_nil c[1] @@ -492,8 +528,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_expression - # Oracle adapter returns floating point value 636.0 after SUM - if current_adapter?(:OracleAdapter) + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter) assert_equal 636, Account.sum("2 * credit_limit") else assert_equal 636, Account.sum("2 * credit_limit").to_i @@ -634,14 +669,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_with_selection_clause - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT credit_limit").sort - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT accounts.credit_limit").sort - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT(credit_limit)").sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort # MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless # an alias is provided. Without the alias, the column cannot be found # and properly typecast. - assert_equal [50 + 53 + 55 + 60], Account.pluck("SUM(DISTINCT(credit_limit)) as credit_limit") + assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit)) as credit_limit")) end def test_plucks_with_ids @@ -653,6 +688,11 @@ 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_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) @@ -721,23 +761,29 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_loaded_relation + Company.attribute_names # Load schema information so we don't query below companies = Company.order(:id).limit(3).load + assert_no_queries do assert_equal ["37signals", "Summit", "Microsoft"], companies.pluck(:name) end end def test_pluck_loaded_relation_multiple_columns + Company.attribute_names # Load schema information so we don't query below companies = Company.order(:id).limit(3).load + assert_no_queries do assert_equal [[1, "37signals"], [2, "Summit"], [3, "Microsoft"]], companies.pluck(:id, :name) end end def test_pluck_loaded_relation_sql_fragment + Company.attribute_names # Load schema information so we don't query below companies = Company.order(:name).limit(3).load + assert_queries 1 do - assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck("DISTINCT name") + assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name")) end end @@ -817,4 +863,46 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.sum(:firm_id) { 1 } end end + + test "#skip_query_cache! for #pluck" do + Account.cache do + assert_queries(1) do + Account.pluck(:credit_limit) + Account.pluck(:credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.pluck(:credit_limit) + Account.all.skip_query_cache!.pluck(:credit_limit) + end + end + end + + test "#skip_query_cache! for a simple calculation" do + Account.cache do + assert_queries(1) do + Account.calculate(:sum, :credit_limit) + Account.calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + end + end + end + + test "#skip_query_cache! for a grouped calculation" do + Account.cache do + assert_queries(1) do + Account.group(:firm_id).calculate(:sum, :credit_limit) + Account.group(:firm_id).calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + end + end + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index b3c86586d0..55c7475f46 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/developer" require "models/computer" diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb index b89294c094..3187e6aed5 100644 --- a/activerecord/test/cases/clone_test.rb +++ b/activerecord/test/cases/clone_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/coders/json_test.rb b/activerecord/test/cases/coders/json_test.rb index d22d93d129..e40d576b39 100644 --- a/activerecord/test/cases/coders/json_test.rb +++ b/activerecord/test/cases/coders/json_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index a26a72712d..4a5559c62f 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index f344c77691..cfe95b2360 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/computer" require "models/developer" @@ -22,7 +24,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -35,7 +37,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -48,11 +50,39 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "cache_key for relation with table alias" do + table_alias = Developer.arel_table.alias("omg_developers") + 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 = developers.where(salary: 100000).order(updated_at: :desc) + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end + test "cache_key for relation with includes" do + comments = Comment.includes(:post).where("posts.type": "Post") + assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key) + end + + test "cache_key for loaded relation with includes" do + comments = Comment.includes(:post).where("posts.type": "Post").load + assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key) + end + test "it triggers at most one query" do developers = Developer.where(name: "David") @@ -111,5 +141,17 @@ module ActiveRecord assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end + + test "cache_key with a relation having distinct and order" do + developers = Developer.distinct.order(:salary).limit(5) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + end + + test "cache_key with a relation having custom select and order" do + developers = Developer.select("name AS dev_name").order("dev_name DESC").limit(5) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + end end end diff --git a/activerecord/test/cases/column_alias_test.rb b/activerecord/test/cases/column_alias_test.rb index 9893ba9580..a883d21fb8 100644 --- a/activerecord/test/cases/column_alias_test.rb +++ b/activerecord/test/cases/column_alias_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 90c8d21c43..cbd2b44589 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index c23be52a6c..584e03d196 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -109,12 +111,14 @@ if ActiveRecord::Base.connection.supports_comments? # And check that these changes are reflected in dump output = dump_table_schema "commenteds" - assert_match %r[create_table "commenteds",.+\s+comment: "A table with comment"], output + assert_match %r[create_table "commenteds",.*\s+comment: "A table with comment"], output assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output assert_match %r[t\.string\s+"obvious"\n], output assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output - assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output - unless current_adapter?(:OracleAdapter) + if current_adapter?(:OracleAdapter) + assert_match %r[t\.integer\s+"rating",\s+precision: 38,\s+comment: "I am running out of imagination"], output + else + assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output end @@ -138,5 +142,27 @@ if ActiveRecord::Base.connection.supports_comments? assert_match %r[t\.string\s+"absent_comment"\n], output assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output end + + def test_change_table_comment + @connection.change_table_comment :commenteds, "Edited table comment" + assert_equal "Edited table comment", @connection.table_comment("commenteds") + end + + def test_change_table_comment_to_nil + @connection.change_table_comment :commenteds, nil + assert_nil @connection.table_comment("commenteds") + end + + def test_change_column_comment + @connection.change_column_comment :commenteds, :name, "Edited column comment" + column = Commented.columns_hash["name"] + assert_equal "Edited column comment", column.comment + end + + def test_change_column_comment_to_nil + @connection.change_column_comment :commenteds, :name, nil + column = Commented.columns_hash["name"] + assert_nil column.comment + end end end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 64189381cb..82c6cf8dea 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 2a71f08d90..603ed63a8c 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -1,8 +1,15 @@ +# frozen_string_literal: true + require "cases/helper" +require "models/person" module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :people + def setup @handler = ConnectionHandler.new @spec_name = "primary" @@ -137,6 +144,33 @@ module ActiveRecord rd.close end + def test_forked_child_doesnt_mangle_parent_connection + object_id = ActiveRecord::Base.connection.object_id + assert ActiveRecord::Base.connection.active? + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + if ActiveRecord::Base.connection.active? + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + end + wr.close + + exit # allow finalizers to run + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + + assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people") + end + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool @pool.schema_cache = @pool.connection.schema_cache @pool.schema_cache.add("posts") @@ -198,15 +232,15 @@ module ActiveRecord def test_a_class_using_custom_pool_and_switching_back_to_primary klass2 = Class.new(Base) { def self.name; "klass2"; end } - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, ActiveRecord::Base.connection pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) - assert_equal klass2.connection.object_id, pool.connection.object_id - refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, pool.connection + refute_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, ActiveRecord::Base.connection end def test_connection_specification_name_should_fallback_to_parent @@ -221,8 +255,8 @@ 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.object_id - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + refute_nil ActiveRecord::Base.connection + assert_same klass2.connection, ActiveRecord::Base.connection end end end diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb index 10a3521c79..f81b73c344 100644 --- a/activerecord/test/cases/connection_adapters/connection_specification_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 8faa67255d..1b64324cc4 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 3657b8340d..02e76ce146 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -1,13 +1,22 @@ +# frozen_string_literal: true + require "cases/helper" +require "support/connection_helper" if current_adapter?(:Mysql2Adapter) module ActiveRecord module ConnectionAdapters class MysqlTypeLookupTest < ActiveRecord::TestCase + include ConnectionHelper + setup do @connection = ActiveRecord::Base.connection end + def teardown + reset_connection + end + def test_boolean_types emulate_booleans(true) do assert_lookup_type :boolean, "tinyint(1)" @@ -47,7 +56,7 @@ if current_adapter?(:Mysql2Adapter) private def assert_lookup_type(type, lookup) - cast_type = @connection.type_map.lookup(lookup) + cast_type = @connection.send(:type_map).lookup(lookup) assert_equal type, cast_type.type end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 106323ccc9..006be9e65d 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index a348c2d783..1c79d776f0 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strings for lookup @@ -80,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_bigint_limit - cast_type = @connection.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 @@ -98,7 +100,7 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin { decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} } end.each do |expected_type, types| types.each do |type| - cast_type = @connection.type_map.lookup(type) + cast_type = @connection.send(:type_map).lookup(type) assert_equal expected_type, cast_type.type assert_equal 2, cast_type.cast(2.1) @@ -109,7 +111,7 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin private def assert_lookup_type(type, lookup) - cast_type = @connection.type_map.lookup(lookup) + cast_type = @connection.send(:type_map).lookup(lookup) assert_equal type, cast_type.type end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index d1e946d401..9d6ecbde78 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "rack" diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 00a0187b57..cb29c578b7 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "concurrent/atomic/count_down_latch" @@ -89,7 +91,7 @@ module ActiveRecord end def test_full_pool_exception - @pool.size.times { @pool.checkout } + @pool.size.times { assert @pool.checkout } assert_raises(ConnectionTimeoutError) do @pool.checkout end @@ -154,6 +156,53 @@ module ActiveRecord @pool.connections.each { |conn| conn.close if conn.in_use? } end + def test_flush + idle_conn = @pool.checkout + recent_conn = @pool.checkout + active_conn = @pool.checkout + + @pool.checkin idle_conn + @pool.checkin recent_conn + + assert_equal 3, @pool.connections.length + + def idle_conn.seconds_idle + 1000 + end + + @pool.flush(30) + + assert_equal 2, @pool.connections.length + + assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__) + ensure + @pool.checkin active_conn + end + + def test_flush_bang + idle_conn = @pool.checkout + recent_conn = @pool.checkout + active_conn = @pool.checkout + _dead_conn = Thread.new { @pool.checkout }.join + + @pool.checkin idle_conn + @pool.checkin recent_conn + + assert_equal 4, @pool.connections.length + + def idle_conn.seconds_idle + 1000 + end + + @pool.flush! + + assert_equal 1, @pool.connections.length + + assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__) + ensure + @pool.checkin active_conn + end + def test_remove_connection conn = @pool.checkout assert conn.in_use? @@ -203,6 +252,14 @@ module ActiveRecord end.join end + def test_checkout_order_is_lifo + conn1 = @pool.checkout + conn2 = @pool.checkout + @pool.checkin conn1 + @pool.checkin conn2 + assert_equal [conn2, conn1], 2.times.map { @pool.checkout } + end + # The connection pool is "fair" if threads waiting for # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request @@ -412,6 +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) @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| @@ -420,6 +478,8 @@ module ActiveRecord end end end + ensure + Thread.report_on_exception = original_report_on_exception if Thread.respond_to?(:report_on_exception) end def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 13b5bae13c..5b80f16a44 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -17,7 +19,7 @@ module ActiveRecord spec "ridiculous://foo?encoding=utf8" end - assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message + assert_match "Could not load the 'ridiculous' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", error.message end # The abstract adapter is used simply to bypass the bit of code that diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 936f26ce04..356afdbd2b 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" require "models/topic" diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 46d7526cc0..e0948f90ac 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/car" diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index 15c8b684e4..f52b26e9ec 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index 66035865be..1c934602ec 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class DatabaseStatementsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/date_test.rb b/activerecord/test/cases/date_test.rb index 2edc0415cd..9f412cdb63 100644 --- a/activerecord/test/cases/date_test.rb +++ b/activerecord/test/cases/date_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index e4a2f9ee17..51f6164138 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index ad7da9de70..b5f35aff0e 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/task" @@ -58,4 +60,17 @@ class DateTimeTest < ActiveRecord::TestCase assert_equal now, task.starting end end + + def test_date_time_with_string_value_with_subsecond_precision + skip unless subsecond_precision_supported? + string_value = "2017-07-04 14:19:00.5" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end + + def test_date_time_with_string_value_with_non_iso_format + string_value = "04/07/2017 2:19pm" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 996d298689..3d11b573f1 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" require "models/default" @@ -51,7 +53,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase def test_default_decimal_number record = DefaultNumber.new - assert_equal BigDecimal.new("2.78"), record.decimal_number + assert_equal BigDecimal("2.78"), record.decimal_number assert_equal "2.78", record.decimal_number_before_type_cast end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index f72e0d2ead..d4408776d3 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" # For booleans require "models/pirate" # For timestamps @@ -299,11 +301,9 @@ class DirtyTest < ActiveRecord::TestCase end def test_virtual_attribute_will_change - assert_deprecated do - parrot = Parrot.create!(name: "Ruby") - parrot.send(:attribute_will_change!, :cancel_save_from_callback) - assert parrot.has_changes_to_save? - end + parrot = Parrot.create!(name: "Ruby") + parrot.send(:attribute_will_change!, :cancel_save_from_callback) + assert parrot.has_changes_to_save? end def test_association_assignment_changes_foreign_key @@ -466,11 +466,10 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present with_partial_writes(Topic) do - Topic.create!(author_name: "Bill", content: { a: "a" }) - topic = Topic.select("id, author_name").first + topic = Topic.create!(author_name: "Bill", content: { a: "a" }) + topic = Topic.select("id, author_name").find(topic.id) topic.update_columns author_name: "John" - topic = Topic.first - assert_not_nil topic.content + assert_not_nil topic.reload.content end end @@ -772,6 +771,13 @@ class DirtyTest < ActiveRecord::TestCase assert person.changed? end + test "attributes not selected are still missing after save" do + person = Person.select(:id).first + assert_raises(ActiveModel::MissingAttributeError) { person.first_name } + assert person.save # calls forget_attribute_assignments + assert_raises(ActiveModel::MissingAttributeError) { person.first_name } + end + test "saved_change_to_attribute? returns whether a change occurred in the last save" do person = Person.create!(first_name: "Sean") @@ -839,15 +845,14 @@ class DirtyTest < ActiveRecord::TestCase assert_equal %w(first_name lock_version updated_at).sort, person.saved_changes.keys.sort end - test "changed? in after callbacks returns true but is deprecated" do + test "changed? in after callbacks returns false" do klass = Class.new(ActiveRecord::Base) do self.table_name = "people" after_save do - ActiveSupport::Deprecation.silence do - raise "changed? should be true" unless changed? - end + raise "changed? should be false" if changed? raise "has_changes_to_save? should be false" if has_changes_to_save? + raise "saved_changes? should be true" unless saved_changes? end end diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index c25089a420..533665d0f4 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class TestRecord < ActiveRecord::Base diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 000ed27efb..73da31996e 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/reply" require "models/topic" @@ -60,10 +62,10 @@ module ActiveRecord topic.attributes = dbtopic.attributes.except("id") - #duped has no timestamp values + # duped has no timestamp values duped = dbtopic.dup - #clear topic timestamp values + # clear topic timestamp values topic.send(:clear_timestamp_attributes) assert_equal topic.changes, duped.changes @@ -98,7 +100,7 @@ module ActiveRecord # temporary change to the topic object topic.updated_at -= 3.days - #dup should not preserve the timestamps if present + # dup should not preserve the timestamps if present new_topic = topic.dup assert_nil new_topic.updated_at assert_nil new_topic.created_at diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 4ef9a125e6..7cda712112 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/book" class EnumTest < ActiveRecord::TestCase - fixtures :books, :authors + fixtures :books, :authors, :author_addresses setup do @book = books(:awdr) @@ -253,12 +255,14 @@ class EnumTest < ActiveRecord::TestCase assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? end - test "_before_type_cast returns the enum label (required for form fields)" do - if @book.status_came_from_user? - assert_equal "published", @book.status_before_type_cast - else - assert_equal "published", @book.status - end + test "_before_type_cast" do + assert_equal 2, @book.status_before_type_cast + assert_equal "published", @book.status + + @book.status = "published" + + assert_equal "published", @book.status_before_type_cast + assert_equal "published", @book.status end test "reserved enum names" do @@ -304,6 +308,24 @@ class EnumTest < ActiveRecord::TestCase end end + test "reserved enum values for relation" do + relation_method_samples = [ + :records, + :to_ary, + :scope_for_create + ] + + relation_method_samples.each do |value| + e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum category: [:other, value] + end + end + assert_match(/You tried to define an enum named .* on the model/, e.message) + end + end + test "overriding enum method should not raise" do assert_nothing_raised do Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index e90669e0c7..b90e6a66c5 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class ErrorsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index ca87e04012..fb698c47cd 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/explain_subscriber" require "active_record/explain_registry" diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 4f6bd9327c..17654027a9 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/car" require "active_support/core_ext/string/strip" @@ -47,7 +49,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_exec_explain_with_binds sqls = %w(foo bar) - binds = [[bind_attribute("wadus", 1)], [bind_attribute("chaflan", 2)]] + binds = [[bind_param("wadus", 1)], [bind_param("chaflan", 2)]] queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do @@ -79,5 +81,9 @@ if ActiveRecord::Base.connection.supports_explain? yield end end + + def bind_param(name, value) + ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) + end end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 3eaa993d45..4039af66d0 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 420f552ef6..95bd987551 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/author" @@ -7,6 +9,7 @@ require "models/company" require "models/tagging" require "models/topic" require "models/reply" +require "models/rating" require "models/entrant" require "models/project" require "models/developer" @@ -117,6 +120,21 @@ class FinderTest < ActiveRecord::TestCase assert_equal "The Fourth Topic of the day", records[2].title end + def test_find_with_ids_with_no_id_passed + exception = assert_raises(ActiveRecord::RecordNotFound) { Topic.find } + assert_equal exception.model, "Topic" + assert_equal exception.primary_key, "id" + end + + def test_find_with_ids_with_id_out_of_range + exception = assert_raises(ActiveRecord::RecordNotFound) do + Topic.find("9999999999999999999999999999999") + end + + assert_equal exception.model, "Topic" + assert_equal exception.primary_key, "id" + end + def test_find_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.find(Topic.last) @@ -154,6 +172,32 @@ class FinderTest < ActiveRecord::TestCase assert_raise(NoMethodError) { Topic.exists?([1, 2]) } end + def test_exists_with_scope + davids = Author.where(name: "David") + assert_equal true, davids.exists? + assert_equal true, davids.exists?(authors(:david).id) + assert_equal false, davids.exists?(authors(:mary).id) + assert_equal false, davids.exists?("42") + assert_equal false, davids.exists?(42) + assert_equal false, davids.exists?(davids.new.id) + + fake = Author.where(name: "fake author") + assert_equal false, fake.exists? + assert_equal false, fake.exists?(authors(:david).id) + end + + def test_exists_uses_existing_scope + post = authors(:david).posts.first + authors = Author.includes(:posts).where(name: "David", posts: { id: post.id }) + assert_equal true, authors.exists?(authors(:david).id) + end + + def test_any_with_scope_on_hash_includes + post = authors(:david).posts.first + categories = Categorization.includes(author: :posts).where(posts: { id: post.id }) + assert_equal true, categories.exists? + end + def test_exists_with_polymorphic_relation post = Post.create!(title: "Post", body: "default", taggings: [Tagging.new(comment: "tagging comment")]) relation = Post.tagged_with_comment("tagging comment") @@ -210,7 +254,7 @@ class FinderTest < ActiveRecord::TestCase # Ensure +exists?+ runs without an error by excluding order value. def test_exists_with_order - assert_equal true, Topic.order("invalid sql here").exists? + assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists? end def test_exists_with_joins @@ -242,6 +286,11 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").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? + end + def test_exists_with_empty_table_and_no_args_given Topic.delete_all assert_equal false, Topic.exists? @@ -514,7 +563,7 @@ class FinderTest < ActiveRecord::TestCase assert_nil Topic.offset(4).second_to_last assert_nil Topic.offset(5).second_to_last - #test with limit + # test with limit assert_nil Topic.limit(1).second assert_nil Topic.limit(1).second_to_last end @@ -616,7 +665,7 @@ class FinderTest < ActiveRecord::TestCase def test_last_with_irreversible_order assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("coalesce(author_name, title)").last + Topic.order(Arel.sql("coalesce(author_name, title)")).last end end @@ -628,12 +677,34 @@ 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_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) @@ -1000,14 +1071,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 @@ -1151,7 +1214,7 @@ class FinderTest < ActiveRecord::TestCase e = assert_raises(ActiveRecord::RecordNotFound) do model.find "Hello", "World!" end - assert_equal "Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)", e.message + assert_equal "Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2).", e.message end end @@ -1177,13 +1240,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal posts(:eager_other), Post.find_by("id = ?", posts(:eager_other).id) end + test "find_by with range conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by(id: posts(:eager_other).id...posts(:misc_by_bob).id) + end + test "find_by returns nil if the record is missing" do assert_nil Post.find_by("1 = 0") end test "find_by with associations" do assert_equal authors(:david), Post.find_by(author: authors(:david)).author - assert_equal authors(:mary) , Post.find_by(author: authors(:mary)).author + assert_equal authors(:mary), Post.find_by(author: authors(:mary)).author end test "find_by doesn't have implicit ordering" do @@ -1232,6 +1299,34 @@ class FinderTest < ActiveRecord::TestCase assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) end + test "#skip_query_cache! for #exists?" do + Topic.cache do + assert_queries(1) do + Topic.exists? + Topic.exists? + end + + assert_queries(2) do + Topic.all.skip_query_cache!.exists? + Topic.all.skip_query_cache!.exists? + end + end + end + + test "#skip_query_cache! for #exists? with a limited eager load" do + Topic.cache do + assert_queries(2) do + Topic.eager_load(:replies).limit(1).exists? + Topic.eager_load(:replies).limit(1).exists? + end + + assert_queries(4) do + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + end + end + end + private def table_with_custom_primary_key yield(Class.new(Toy) do diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index 533edcc2e0..ff99988cb5 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "tempfile" diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index b499e60922..8e8a49af8e 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/admin" require "models/admin/account" @@ -130,7 +132,7 @@ class FixturesTest < ActiveRecord::TestCase def test_no_args_record_returns_all_without_array all_binaries = binaries assert_kind_of(Array, all_binaries) - assert_equal 1, binaries.length + assert_equal 2, binaries.length end def test_nil_raises @@ -245,7 +247,7 @@ class FixturesTest < ActiveRecord::TestCase def test_nonexistent_fixture_file nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere" - #sanity check to make sure that this file never exists + # sanity check to make sure that this file never exists assert Dir[nonexistent_fixture_path + "*"].empty? assert_raise(Errno::ENOENT) do @@ -311,6 +313,7 @@ class FixturesTest < ActiveRecord::TestCase data.force_encoding("ASCII-8BIT") data.freeze assert_equal data, @flowers.data + assert_equal data, @binary_helper.data end def test_serialized_fixtures @@ -395,6 +398,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) class FixturesResetPkSequenceTest < ActiveRecord::TestCase fixtures :accounts fixtures :companies + self.use_transactional_tests = false def setup @instances = [Account.new(credit_limit: 50), Company.new(name: "RoR Consulting"), Course.new(name: "Test")] @@ -839,6 +843,8 @@ class FasterFixturesTest < ActiveRecord::TestCase end class FoxyFixturesTest < ActiveRecord::TestCase + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + self.use_transactional_tests = false fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index ffa3f63e0d..101fa118c8 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/hash/indifferent_access" diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb index 365d4576dd..5e503272e1 100644 --- a/activerecord/test/cases/habtm_destroy_order_test.rb +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/lesson" require "models/student" diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 5a3b8e3fb5..6ea02ac191 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "config" require "stringio" @@ -141,6 +143,8 @@ def load_schema if File.exist?(adapter_specific_schema_file) load adapter_specific_schema_file end + + ActiveRecord::FixtureSet.reset_cache ensure $stdout = original_stdout end diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e107ff2362..e7778af55b 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/connection_helper" diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb index 7f03c5b23d..22981c142a 100644 --- a/activerecord/test/cases/i18n_test.rb +++ b/activerecord/test/cases/i18n_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index fb5a7bcc31..ff4385c8b4 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/company" @@ -145,12 +147,16 @@ class InheritanceTest < ActiveRecord::TestCase # Concrete subclass of AR::Base. assert 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? + # Abstract subclass of a concrete class which has a type column. # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !StiPost.descends_from_active_record? + assert !AbstractStiPost.descends_from_active_record? - # Concrete subclasses an abstract class which has a type column. - assert !SubStiPost.descends_from_active_record? + # Concrete subclass of an abstract class which has a type column. + assert !SubAbstractStiPost.descends_from_active_record? end def test_company_descends_from_active_record @@ -170,7 +176,8 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Post, Post.base_class assert_equal Post, SpecialPost.base_class assert_equal Post, StiPost.base_class - assert_equal SubStiPost, SubStiPost.base_class + assert_equal Post, SubStiPost.base_class + assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class end def test_abstract_inheritance_base_class @@ -273,6 +280,21 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Firm, firm.class end + def test_where_new_with_subclass + firm = Company.where(type: "Firm").new + assert_equal Firm, firm.class + end + + def test_where_create_with_subclass + firm = Company.where(type: "Firm").create(name: "Basecamp") + assert_equal Firm, firm.class + end + + def test_where_create_bang_with_subclass + firm = Company.where(type: "Firm").create!(name: "Basecamp") + assert_equal Firm, firm.class + end + def test_new_with_abstract_class e = assert_raises(NotImplementedError) do AbstractCompany.new @@ -295,6 +317,30 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "Account") } end + def test_where_new_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").new } + end + + def test_where_new_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").new } + end + + def test_where_create_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create } + end + + def test_where_create_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create } + end + + def test_where_create_bang_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create! } + end + + def test_where_create_bang_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create! } + end + def test_new_with_unrelated_namespaced_type without_store_full_sti_class do e = assert_raises ActiveRecord::SubclassNotFound do @@ -418,7 +464,8 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = 1/) do + bind_param = Arel::Nodes::BindParam.new(nil) + assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = (?:#{Regexp.quote(bind_param.to_sql)}|1)/) do Account.all.merge!(includes: :firm).find(1) end end diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb new file mode 100644 index 0000000000..e6e8468757 --- /dev/null +++ b/activerecord/test/cases/instrumentation_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/book" + +module ActiveRecord + class InstrumentationTest < ActiveRecord::TestCase + def test_payload_name_on_load + Book.create(name: "test book") + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if event.payload[:sql].match "SELECT" + assert_equal "Book Load", event.payload[:name] + end + end + Book.first + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_payload_name_on_create + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if event.payload[:sql].match "INSERT" + assert_equal "Book Create", event.payload[:name] + end + end + Book.create(name: "test book") + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_payload_name_on_update + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if event.payload[:sql].match "UPDATE" + assert_equal "Book Update", event.payload[:name] + end + end + book = Book.create(name: "test book") + book.update_attribute(:name, "new name") + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_payload_name_on_update_all + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if event.payload[:sql].match "UPDATE" + assert_equal "Book Update All", event.payload[:name] + end + end + Book.create(name: "test book") + Book.update_all(name: "new name") + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_payload_name_on_destroy + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if event.payload[:sql].match "DELETE" + assert_equal "Book Destroy", event.payload[:name] + end + end + book = Book.create(name: "test book") + book.destroy + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + end +end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 9104976126..36cd63c4d4 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/company" require "models/developer" diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 1367af2859..a1be9c2780 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" if current_adapter?(:Mysql2Adapter) diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index cc3951e2ba..ebe0b0aa87 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Horse < ActiveRecord::Base @@ -159,6 +161,15 @@ module ActiveRecord end end + class UpOnlyMigration < SilentMigration + def change + add_column :horses, :oldie, :integer, default: 0 + up_only { execute "update horses set oldie = 1" } + end + end + + self.use_transactional_tests = false + setup do @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false end @@ -293,6 +304,8 @@ module ActiveRecord migration2.migrate(:down) assert_equal false, Horse.connection.extension_enabled?("hstore") + ensure + enable_extension!("hstore", ActiveRecord::Base.connection) end end @@ -374,5 +387,23 @@ module ActiveRecord "horses_index_named index should not exist" end end + + def test_up_only + InvertibleMigration.new.migrate(:up) + horse1 = Horse.create + # populates existing horses with oldie = 1 but new ones have default 0 + UpOnlyMigration.new.migrate(:up) + Horse.reset_column_information + horse1.reload + horse2 = Horse.create + + assert 1, horse1.oldie # created before migration + assert 0, horse2.oldie # created after migration + + UpOnlyMigration.new.migrate(:down) # should be no error + connection = ActiveRecord::Base.connection + assert !connection.column_exists?(:horses, :oldie) + Horse.reset_column_information + end end end diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb index e5848b45f8..afc39d0420 100644 --- a/activerecord/test/cases/json_attribute_test.rb +++ b/activerecord/test/cases/json_attribute_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "cases/json_shared_test_cases" @@ -17,14 +19,14 @@ class JsonAttributeTest < ActiveRecord::TestCase def setup super @connection.create_table("json_data_type") do |t| - t.text "payload" - t.text "settings" + t.string "payload" + t.string "settings" end end private def column_type - :text + :string end def klass diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 9b4b61b16e..52fe488cd5 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/contact" require "models/post" @@ -160,10 +162,8 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_serializable_hash_should_not_modify_options_in_argument - options = { only: :name } - @contact.serializable_hash(options) - - assert_nil options[:except] + options = { only: :name }.freeze + assert_nothing_raised { @contact.serializable_hash(options) } end end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index 9a1c1c3f3f..b0c0f2c283 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "support/schema_dumping_helper" module JSONSharedTestCases @@ -21,7 +23,7 @@ module JSONSharedTestCases def test_column column = klass.columns_hash["payload"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type type = klass.type_for_attribute("payload") assert_not type.binary? @@ -34,7 +36,7 @@ module JSONSharedTestCases klass.reset_column_information column = klass.columns_hash["users"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type end def test_schema_dumping @@ -64,26 +66,26 @@ module JSONSharedTestCases end def test_rewrite - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first x.payload = { '"a\'' => "b" } assert x.save! end def test_select - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first assert_equal({ "k" => "v" }, x.payload) end def test_select_multikey - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')) x = klass.first assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) end def test_null_json - @connection.execute("insert into json_data_type (payload) VALUES(null)") + @connection.execute(insert_statement_per_database("null")) x = klass.first assert_nil(x.payload) end @@ -105,13 +107,13 @@ module JSONSharedTestCases end def test_select_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first assert_equal(["v0", { "k1" => "v1" }], x.payload) end def test_rewrite_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first x.payload = ["v1", { "k2" => "v2" }, "v3"] assert x.save! @@ -214,8 +216,54 @@ module JSONSharedTestCases assert_equal true, json.payload end + def test_not_compatible_with_serialize_json + new_klass = Class.new(klass) do + serialize :payload, JSON + end + assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do + new_klass.new + end + end + + class MySettings + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end + + def test_json_with_serialized_attributes + new_klass = Class.new(klass) do + serialize :settings, MySettings + end + + new_klass.create!(settings: MySettings.new("one" => "two")) + record = new_klass.first + + assert_instance_of MySettings, record.settings + assert_equal({ "one" => "two" }, record.settings.to_hash) + + record.settings = MySettings.new("three" => "four") + record.save! + + assert_equal({ "three" => "four" }, record.reload.settings.to_hash) + end + private def klass JsonDataType end + + def assert_type_match(type, sql_type) + native_type = ActiveRecord::Base.connection.native_database_types[type][:name] + assert_match %r(\A#{native_type}\b), sql_type + end + + def insert_statement_per_database(values) + if current_adapter?(:OracleAdapter) + "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')" + else + "insert into json_data_type (payload) VALUES ('#{values}')" + end + end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 2fc52393f2..3701be4b11 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "thread" require "cases/helper" require "models/person" @@ -397,11 +399,43 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_counter_cache_with_touch_and_lock_version + car = Car.create! + + assert_equal 0, car.wheels_count + assert_equal 0, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.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 + + previously_car_updated_at = car.updated_at + car.wheels.first.update(size: 42) + + assert_equal 1, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 2, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.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 + end + def test_polymorphic_destroy_with_dependencies_and_lock_version car = Car.create! assert_difference "car.wheels.count" do - car.wheels << Wheel.create! + car.wheels.create end assert_difference "car.wheels.count", -1 do car.reload.destroy @@ -563,18 +597,18 @@ unless in_memory_db? end end - # Locking a record reloads it. - def test_sane_lock_method + def test_lock_does_not_raise_when_the_object_is_not_dirty + person = Person.find 1 assert_nothing_raised do - Person.transaction do - person = Person.find 1 - old, person.first_name = person.first_name, "fooman" - # Locking a dirty record is deprecated - assert_deprecated do - person.lock! - end - assert_equal old, person.first_name - end + person.lock! + end + end + + def test_lock_raises_when_the_record_is_dirty + person = Person.find 1 + person.first_name = "fooman" + assert_raises(RuntimeError) do + person.lock! end end @@ -609,14 +643,12 @@ unless in_memory_db? end end - if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - def test_no_locks_no_wait - first, second = duel { Person.find 1 } - assert first.end > second.end - end - - private + def test_no_locks_no_wait + first, second = duel { Person.find 1 } + assert first.end > second.end + end + private def duel(zzz = 5) t0, t1, t2, t3 = nil, nil, nil, nil @@ -644,6 +676,5 @@ unless in_memory_db? assert t3 > t2 [t0.to_f..t1.to_f, t2.to_f..t3.to_f] end - end end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index b80257962c..e2742ed33e 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/binary" require "models/developer" @@ -31,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase super end - def debug(message) - @debugs << message + def debug(progname = nil, &block) + @debugs << progname + super end end @@ -169,6 +172,22 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_vebose_query_logs + ActiveRecord::Base.verbose_query_logs = true + + logger = TestDebugLogSubscriber.new + logger.sql(Event.new(0, sql: "hi mom!")) + assert_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!")) + assert_no_match(/↳/, @logger.logged(:debug).last) + end + def test_cached_queries ActiveRecord::Base.cache do Developer.all.load diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 1d305fa11f..38a906c8f5 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -262,17 +264,18 @@ module ActiveRecord t.column :foo, :timestamp end - klass = Class.new(ActiveRecord::Base) - klass.table_name = "testings" + column = connection.columns(:testings).find { |c| c.name == "foo" } - assert_equal :datetime, klass.columns_hash["foo"].type + assert_equal :datetime, column.type if current_adapter?(:PostgreSQLAdapter) - assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + assert_equal "timestamp without time zone", column.sql_type elsif current_adapter?(:Mysql2Adapter) - assert_equal "timestamp", klass.columns_hash["foo"].sql_type + assert_equal "timestamp", column.sql_type + elsif current_adapter?(:OracleAdapter) + assert_equal "TIMESTAMP(6)", column.sql_type else - assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type + assert_equal connection.type_to_sql("datetime"), column.sql_type end end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index ec817a579b..034bf32165 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/migration/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 48df931543..3022121f4c 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/migration/helper" module ActiveRecord @@ -43,11 +45,11 @@ module ActiveRecord assert_nil TestModel.columns_hash["description"].limit end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_unabstracted_database_dependent_types - add_column :test_models, :intelligence_quotient, :tinyint + add_column :test_models, :intelligence_quotient, :smallint TestModel.reset_column_information - assert_match(/tinyint/, TestModel.columns_hash["intelligence_quotient"].sql_type) + assert_match(/smallint/, TestModel.columns_hash["intelligence_quotient"].sql_type) end end @@ -78,7 +80,7 @@ module ActiveRecord TestModel.delete_all # Now use the Rails insertion - TestModel.create wealth: BigDecimal.new("12345678901234567890.0123456789") + TestModel.create wealth: BigDecimal("12345678901234567890.0123456789") # SELECT row = TestModel.first @@ -97,7 +99,21 @@ module ActiveRecord assert_equal 7, wealth_column.scale end + # Test SQLite3 adapter specifically for decimal types with precision and scale + # attributes, since these need to be maintained in schema but aren't actually + # used in SQLite3 itself if current_adapter?(:SQLite3Adapter) + def test_change_column_with_new_precision_and_scale + connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7 + + connection.change_column "test_models", "wealth", :decimal, precision: 12, scale: 8 + TestModel.reset_column_information + + wealth_column = TestModel.columns_hash["wealth"] + assert_equal 12, wealth_column.precision + assert_equal 8, wealth_column.scale + end + def test_change_column_preserve_other_column_precision_and_scale connection.add_column "test_models", "last_name", :string connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7 @@ -130,7 +146,7 @@ module ActiveRecord TestModel.create first_name: "bob", last_name: "bobsen", bio: "I was born ....", age: 18, height: 1.78, - wealth: BigDecimal.new("12345678901234567890.0123456789"), + wealth: BigDecimal("12345678901234567890.0123456789"), birthday: 18.years.ago, favorite_day: 10.days.ago, moment_of_truth: "1782-10-10 21:40:18", male: true @@ -143,7 +159,7 @@ module ActiveRecord # Test for 30 significant digits (beyond the 16 of float), 10 of them # after the decimal place. - assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth + assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth assert_equal true, bob.male? diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index f2162d91b1..1c62a68cf9 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -50,6 +52,16 @@ module ActiveRecord conn.change_column :testings, :second, :integer, after: :third assert_equal %w(first third second), conn.columns(:testings).map(&:name) end + + def test_add_reference_with_positioning_first + conn.add_reference :testings, :new, polymorphic: true, first: true + assert_equal %w(new_id new_type first second third), conn.columns(:testings).map(&:name) + end + + def test_add_reference_with_positioning_after + conn.add_reference :testings, :new, polymorphic: true, after: :first + assert_equal %w(first new_id new_type second third), conn.columns(:testings).map(&:name) + end end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 2329888345..8ca20b6172 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/migration/helper" module ActiveRecord @@ -140,6 +142,10 @@ module ActiveRecord end def test_remove_column_with_multi_column_index + # MariaDB starting with 10.2.8 + # Dropping a column that is part of a multi-column UNIQUE constraint is not permitted. + skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.version >= "10.2.8" + add_column "test_models", :hat_size, :integer add_column "test_models", :hat_style, :string, limit: 100 add_index "test_models", ["hat_style", "hat_size"], unique: true diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 007926f1b9..58bc558619 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -211,6 +213,11 @@ module ActiveRecord assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end + def test_invert_add_index_with_algorithm_option + remove = @recorder.inverse_of :add_index, [:table, :one, algorithm: :concurrently] + assert_equal [:remove_index, [:table, { column: :one, algorithm: :concurrently }]], remove + end + def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 596a21dcbc..26d3b3e29d 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -14,7 +16,7 @@ module ActiveRecord ActiveRecord::Migration.verbose = false connection.create_table :testings do |t| - t.column :foo, :string, limit: 100 + t.column :foo, :string, limit: 5 t.column :bar, :string, limit: 100 end end @@ -124,15 +126,32 @@ module ActiveRecord end assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end + + if current_adapter?(:PostgreSQLAdapter) + class Testing < ActiveRecord::Base + end + + def test_legacy_change_column_with_null_executes_update + migration = Class.new(ActiveRecord::Migration[5.1]) { + def migrate(x) + change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar" + end + }.new + + Testing.create! + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_equal ["foobar"], Testing.all.map(&:foo) + ensure + ActiveRecord::Base.clear_cache! + end + end end end end -class LegacyPrimaryKeyTest < ActiveRecord::TestCase +module LegacyPrimaryKeyTestCases include SchemaDumpingHelper - self.use_transactional_tests = false - class LegacyPrimaryKey < ActiveRecord::Base end @@ -150,7 +169,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase end def test_legacy_primary_key_should_be_auto_incremented - @migration = Class.new(ActiveRecord::Migration[5.0]) { + @migration = Class.new(migration_class) { def change create_table :legacy_primary_keys do |t| t.references :legacy_ref @@ -160,9 +179,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase @migration.migrate(:up) - legacy_pk = LegacyPrimaryKey.columns_hash["id"] - assert_not legacy_pk.bigint? - assert_not legacy_pk.null + assert_legacy_primary_key legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] assert_not legacy_ref.bigint? @@ -180,7 +197,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase def test_legacy_integer_primary_key_should_not_be_auto_incremented skip if current_adapter?(:SQLite3Adapter) - @migration = Class.new(ActiveRecord::Migration[5.0]) { + @migration = Class.new(migration_class) { def change create_table :legacy_primary_keys, id: :integer do |t| end @@ -197,9 +214,85 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema end + def test_legacy_primary_key_in_create_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.primary_key :id + end + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_legacy_primary_key_in_change_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy + end + change_table :legacy_primary_keys do |t| + t.primary_key :id + end + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_add_column_with_legacy_primary_key_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy + end + add_column :legacy_primary_keys, :id, :primary_key + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_legacy_join_table_foreign_keys_should_be_integer + @migration = Class.new(migration_class) { + def change + create_join_table :apples, :bananas do |t| + end + end + }.new + + @migration.migrate(:up) + + schema = dump_table_schema "apples_bananas" + assert_match %r{integer "apple_id", null: false}, schema + assert_match %r{integer "banana_id", null: false}, schema + end + + def test_legacy_join_table_column_options_should_be_overwritten + @migration = Class.new(migration_class) { + def change + create_join_table :apples, :bananas, column_options: { type: :bigint } do |t| + end + end + }.new + + @migration.migrate(:up) + + schema = dump_table_schema "apples_bananas" + assert_match %r{bigint "apple_id", null: false}, schema + assert_match %r{bigint "banana_id", null: false}, schema + end + if current_adapter?(:Mysql2Adapter) def test_legacy_bigint_primary_key_should_be_auto_incremented - @migration = Class.new(ActiveRecord::Migration[5.0]) { + @migration = Class.new(migration_class) { def change create_table :legacy_primary_keys, id: :bigint end @@ -216,7 +309,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase end else def test_legacy_bigint_primary_key_should_not_be_auto_incremented - @migration = Class.new(ActiveRecord::Migration[5.0]) { + @migration = Class.new(migration_class) { def change create_table :legacy_primary_keys, id: :bigint do |t| end @@ -233,4 +326,44 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema end end + + private + def assert_legacy_primary_key + assert_equal "id", LegacyPrimaryKey.primary_key + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + + assert_equal :integer, legacy_pk.type + assert_not legacy_pk.bigint? + assert_not legacy_pk.null + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema + end + end +end + +module LegacyPrimaryKeyTest + class V5_0 < ActiveRecord::TestCase + include LegacyPrimaryKeyTestCases + + self.use_transactional_tests = false + + private + def migration_class + ActiveRecord::Migration[5.0] + end + end + + class V4_2 < ActiveRecord::TestCase + include LegacyPrimaryKeyTestCases + + self.use_transactional_tests = false + + private + def migration_class + ActiveRecord::Migration[4.2] + end + end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index c4896f3d6e..77d32a24a5 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 7762d37915..079be04946 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -225,6 +227,74 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end end + if ActiveRecord::Base.connection.supports_validate_constraints? + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + refute 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? + + @connection.validate_foreign_key :astronauts, :rockets + assert @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? + + @connection.validate_foreign_key :astronauts, column: "rocket_id" + assert @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? + + @connection.validate_foreign_key :astronauts, column: :rocket_id + assert @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? + + @connection.validate_foreign_key :astronauts, name: "fancy_named_fk" + assert @connection.foreign_keys("astronauts").first.validated? + end + + def test_validate_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.validate_foreign_key :astronauts, :rockets + end + end + + def test_validate_constraint_by_name + @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? + end + else + # Foreign key should still be created, but should not be invalid + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert fk.validated? + end + end + def test_schema_dumping @connection.add_foreign_key :astronauts, :rockets output = dump_table_schema "astronauts" diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index 9c0fa7339d..c056199140 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index f10fcf1398..b25c6d84bc 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -31,10 +33,8 @@ module ActiveRecord connection.add_index(table_name, [:foo], name: "old_idx") connection.rename_index(table_name, "old_idx", "new_idx") - assert_deprecated do - assert_not connection.index_name_exists?(table_name, "old_idx", false) - assert connection.index_name_exists?(table_name, "new_idx", true) - end + assert_not connection.index_name_exists?(table_name, "old_idx") + assert connection.index_name_exists?(table_name, "new_idx") end def test_rename_index_too_long diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 3d7c7ad469..28f4cc124b 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 6970fdcc87..d0066f68be 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 718b9a0613..7a092103c7 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" if ActiveRecord::Base.connection.supports_foreign_keys_in_create? diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 2866cabab6..e41377d817 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index e9eb9968cb..769241ba12 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/migration/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 5da3ad33a3..dfce266253 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/migration/helper" module ActiveRecord diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index eff6e09eb7..a3ebc8070a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "cases/migration/helper" require "bigdecimal/util" @@ -799,8 +801,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.integer :age end - # Adding an index fires a query every time to check if an index already exists or not - assert_queries(3) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "PostgreSQLAdapter" => 2, + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.index :username, unique: true, name: :awesome_username_index t.index [:name, :age] @@ -824,7 +833,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert index(:index_delete_me_on_name) - assert_queries(3) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "PostgreSQLAdapter" => 2, + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.remove_index :name t.index :name, name: :new_name_index, unique: true @@ -846,10 +863,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert ! column(:name).default assert_equal :date, column(:birthdate).type - # One query for columns (delete_me table) - # One query for primary key (delete_me table) - # One query to do the bulk change - assert_queries(3, ignore_none: true) do + 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 + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + 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 @@ -912,7 +934,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) expected = "# This migration comes from bukkits (originally 1)" - assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[0].chomp + assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[1].chomp files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy") @@ -1104,21 +1126,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_unknown_migration_version_should_raise_an_argument_error assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } end - - def test_deprecate_initialize_internal_tables - assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table } - assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table } - end - - def test_deprecate_migration_keys - assert_deprecated { ActiveRecord::Base.connection.migration_keys } - end - - def test_deprecate_supports_migrations - assert_deprecated { ActiveRecord::Base.connection.supports_migrations? } - end - - def test_deprecate_schema_migrations_table_name - assert_deprecated { ActiveRecord::Migrator.schema_migrations_table_name } - end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2e4b454a86..1047ba1367 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "cases/migration/helper" @@ -64,6 +66,26 @@ class MigratorTest < ActiveRecord::TestCase list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] ActiveRecord::Migrator.new(:up, list, 3).run end + + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] + ActiveRecord::Migrator.new(:up, list, -1).run + end + + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] + ActiveRecord::Migrator.new(:up, list, 0).run + end + + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] + ActiveRecord::Migrator.new(:up, list, 3).migrate + end + + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] + ActiveRecord::Migrator.new(:up, list, -1).migrate + end end def test_finds_migrations diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index a8af8e30f7..fdb8ac6ab3 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mixin < ActiveRecord::Base @@ -10,10 +12,6 @@ class TouchTest < ActiveRecord::TestCase travel_to Time.now end - teardown do - travel_back - end - def test_update stamped = Mixin.new diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index f8a7bab35f..060d555607 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/company_in_module" require "models/shop" diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index ceb5724377..59be4dc5a8 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/customer" @@ -271,6 +273,12 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase ensure Topic.reset_column_information end + + def test_multiparameter_attributes_setting_time_attribute + topic = Topic.new("bonus_time(4i)" => "01", "bonus_time(5i)" => "05") + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end end def test_multiparameter_attributes_on_time_with_empty_seconds @@ -285,14 +293,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end end - unless current_adapter? :OracleAdapter - def test_multiparameter_attributes_setting_time_attribute - topic = Topic.new("bonus_time(4i)" => "01", "bonus_time(5i)" => "05") - assert_equal 1, topic.bonus_time.hour - assert_equal 5, topic.bonus_time.min - end - end - def test_multiparameter_attributes_setting_date_attribute topic = Topic.new("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11") assert_equal 1952, topic.written_on.year @@ -300,13 +300,34 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase assert_equal 11, topic.written_on.day end + def test_create_with_multiparameter_attributes_setting_date_attribute + topic = Topic.create_with("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11").new + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + end + def test_multiparameter_attributes_setting_date_and_time_attribute topic = Topic.new( - "written_on(1i)" => "1952", - "written_on(2i)" => "3", - "written_on(3i)" => "11", - "written_on(4i)" => "13", - "written_on(5i)" => "55") + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55") + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + assert_equal 13, topic.written_on.hour + assert_equal 55, topic.written_on.min + end + + def test_create_with_multiparameter_attributes_setting_date_and_time_attribute + topic = Topic.create_with( + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55").new assert_equal 1952, topic.written_on.year assert_equal 3, topic.written_on.month assert_equal 11, topic.written_on.day @@ -364,4 +385,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase assert_equal("address", ex.errors[0].attribute) end + + def test_multiparameter_assigned_attributes_did_not_come_from_user + topic = Topic.new( + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55", + ) + refute_predicate topic, :written_on_came_from_user? + end end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index e3bb51bd77..192d2f5251 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/entrant" require "models/bird" @@ -90,14 +92,9 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal "Ruby Developer", Entrant.find(1).name end - def test_arel_table_engines - assert_not_equal Entrant.arel_engine, Bird.arel_engine - assert_not_equal Entrant.arel_engine, Course.arel_engine - end - def test_connection - assert_equal Entrant.arel_engine.connection.object_id, Bird.arel_engine.connection.object_id - assert_not_equal Entrant.arel_engine.connection.object_id, Course.arel_engine.connection.object_id + assert_same Entrant.connection, Bird.connection + assert_not_same Entrant.connection, Course.connection end unless in_memory_db? diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 154faa56aa..a2ccb603a9 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/pirate" require "models/ship" diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index b9d2acbed2..f04c68b08f 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/pirate" require "models/bird" diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb new file mode 100644 index 0000000000..17527568f8 --- /dev/null +++ b/activerecord/test/cases/null_relation_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/developer" +require "models/comment" +require "models/post" +require "models/topic" + +class NullRelationTest < ActiveRecord::TestCase + fixtures :posts, :comments + + def test_none + assert_no_queries(ignore_none: false) do + assert_equal [], Developer.none + assert_equal [], Developer.all.none + end + end + + def test_none_chainable + assert_no_queries(ignore_none: false) do + assert_equal [], Developer.none.where(name: "David") + end + end + + def test_none_chainable_to_existing_scope_extension_method + assert_no_queries(ignore_none: false) do + assert_equal 1, Topic.anonymous_extension.none.one + end + end + + def test_none_chained_to_methods_firing_queries_straight_to_db + assert_no_queries(ignore_none: false) do + assert_equal [], Developer.none.pluck(:id, :name) + assert_equal 0, Developer.none.delete_all + assert_equal 0, Developer.none.update_all(name: "David") + assert_equal 0, Developer.none.delete(1) + assert_equal false, Developer.none.exists?(1) + end + end + + def test_null_relation_content_size_methods + assert_no_queries(ignore_none: false) do + assert_equal 0, Developer.none.size + assert_equal 0, Developer.none.count + assert_equal true, Developer.none.empty? + assert_equal true, Developer.none.none? + assert_equal false, Developer.none.any? + assert_equal false, Developer.none.one? + assert_equal false, Developer.none.many? + end + end + + def test_null_relation_metadata_methods + assert_equal "", Developer.none.to_sql + assert_equal({}, Developer.none.where_values_hash) + end + + def test_null_relation_where_values_hash + assert_equal({ "salary" => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) + end + + [:count, :sum].each do |method| + define_method "test_null_relation_#{method}" do + assert_no_queries(ignore_none: false) do + assert_equal 0, Comment.none.public_send(method, :id) + assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) + end + end + end + + [:average, :minimum, :maximum].each do |method| + define_method "test_null_relation_#{method}" do + assert_no_queries(ignore_none: false) do + assert_nil Comment.none.public_send(method, :id) + assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) + end + end + end + + def test_null_relation_in_where_condition + assert_operator Comment.count, :>, 0 # precondition, make sure there are comments. + assert_equal 0, Comment.where(post_id: Post.none).count + end +end diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index 76b97033af..f917c8f727 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/numeric_data" diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 5895c51714..0fa8ea212f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/aircraft" require "models/post" @@ -57,13 +59,42 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_all_with_order_and_limit_updates_subset_only author = authors(:david) - assert_nothing_raised do - assert_equal 1, author.posts_sorted_by_id_limited.size - assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size - assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:welcome).body - assert_not_equal "bulk update!", posts(:thinking).body - end + limited_posts = author.posts_sorted_by_id_limited + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body + end + + def test_update_all_with_order_and_limit_and_offset_updates_subset_only + author = authors(:david) + limited_posts = author.posts_sorted_by_id_limited.offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:thinking).body + assert_not_equal "bulk update!", posts(:welcome).body + end + + def test_delete_all_with_order_and_limit_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } + assert posts(:thinking) + end + + def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } + assert posts(:welcome) end end @@ -71,11 +102,43 @@ class PersistenceTest < ActiveRecord::TestCase topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } updated = Topic.update(topic_data.keys, topic_data.values) - assert_equal 2, updated.size + assert_equal [1, 2], updated.map(&:id) + assert_equal "1 updated", Topic.find(1).content + assert_equal "2 updated", Topic.find(2).content + end + + def test_update_many_with_duplicated_ids + updated = Topic.update([1, 1, 2], [ + { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" } + ]) + + assert_equal [1, 1, 2], updated.map(&:id) assert_equal "1 updated", Topic.find(1).content assert_equal "2 updated", Topic.find(2).content end + def test_update_many_with_invalid_id + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} } + + assert_raise(ActiveRecord::RecordNotFound) do + Topic.update(topic_data.keys, topic_data.values) + end + + assert_not_equal "1 updated", Topic.find(1).content + assert_not_equal "2 updated", Topic.find(2).content + end + + def test_class_level_update_is_affected_by_scoping + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } + + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + end + + assert_not_equal "1 updated", Topic.find(1).content + assert_not_equal "2 updated", Topic.find(2).content + end + def test_delete_all assert Topic.count > 0 @@ -83,27 +146,31 @@ class PersistenceTest < ActiveRecord::TestCase end def test_delete_all_with_joins_and_where_part_is_hash - where_args = { toys: { name: "Bone" } } - count = Pet.joins(:toys).where(where_args).count + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end - assert_equal count, 1 - assert_equal count, Pet.joins(:toys).where(where_args).delete_all + def test_delete_all_with_joins_and_where_part_is_not_hash + pets = Pet.joins(:toys).where("toys.name = ?", "Bone") + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all end def test_delete_all_with_left_joins - where_args = { toys: { name: "Bone" } } - count = Pet.left_joins(:toys).where(where_args).count + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) - assert_equal count, 1 - assert_equal count, Pet.left_joins(:toys).where(where_args).delete_all + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all end - def test_delete_all_with_joins_and_where_part_is_not_hash - where_args = ["toys.name = ?", "Bone"] - count = Pet.joins(:toys).where(where_args).count + def test_delete_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - assert_equal count, 1 - assert_equal count, Pet.joins(:toys).where(where_args).delete_all + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all end def test_increment_attribute @@ -160,15 +227,25 @@ class PersistenceTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.all.merge!(order: "id").find([2, 3]) + clients = Client.find([2, 3]) assert_difference("Client.count", -2) do - destroyed = Client.destroy([2, 3]).sort_by(&:id) + destroyed = Client.destroy([2, 3]) assert_equal clients, destroyed assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" end end + def test_destroy_many_with_invalid_id + clients = Client.find([2, 3]) + + assert_raise(ActiveRecord::RecordNotFound) do + Client.destroy([2, 3, 99999]) + end + + assert_equal clients, Client.find([2, 3]) + end + def test_becomes assert_kind_of Reply, topics(:first).becomes(Reply) assert_equal "The First Topic", topics(:first).becomes(Reply).title @@ -305,12 +382,13 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.instantiate( - "attributes" => { - "title" => "Another New Topic", - "does_not_exist" => "test" - } + "title" => "Another New Topic", + "does_not_exist" => "test" ) + topic = topic.dup # reset @new_record assert_nothing_raised { topic.save } + assert topic.persisted? + assert_equal "Another New Topic", topic.reload.title end def test_create_through_factory_with_block @@ -357,6 +435,8 @@ 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_equal "A New Topic", topic_reloaded.reload.title end def test_update_for_record_with_only_primary_key @@ -437,6 +517,13 @@ class PersistenceTest < ActiveRecord::TestCase assert_not_nil Topic.find(2) end + def test_delete_isnt_affected_by_scoping + topic = Topic.find(1) + assert_difference("Topic.count", -1) do + Topic.where("1=0").scoping { topic.delete } + end + end + def test_destroy topic = Topic.find(1) assert_equal topic, topic.destroy, "topic.destroy did not return self" @@ -451,10 +538,18 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end - def test_record_not_found_exception + def test_find_raises_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end + def test_update_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) } + end + + def test_destroy_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) } + end + def test_update_all assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") assert_equal "bulk updated!", Topic.find(1).content @@ -478,17 +573,24 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_all_with_joins - where_args = { toys: { name: "Bone" } } - count = Pet.left_joins(:toys).where(where_args).count + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) - assert_equal count, Pet.joins(:toys).where(where_args).update_all(name: "Bob") + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") end def test_update_all_with_left_joins - where_args = { toys: { name: "Bone" } } - count = Pet.left_joins(:toys).where(where_args).count + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - assert_equal count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob") + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") end def test_update_all_with_non_standard_table_name @@ -531,6 +633,9 @@ class PersistenceTest < ActiveRecord::TestCase Topic.find(1).update_attribute(:approved, false) assert !Topic.find(1).approved? + + Topic.find(1).update_attribute(:change_approved_before_save, true) + assert Topic.find(1).approved? end def test_update_attribute_for_readonly_attribute @@ -898,18 +1003,41 @@ class PersistenceTest < ActiveRecord::TestCase should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") Topic.find(1).replies << should_be_destroyed_reply - Topic.destroy(1) + topic = Topic.destroy(1) + assert topic.destroyed? + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } end + def test_class_level_destroy_is_affected_by_scoping + should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_not_be_destroyed_reply + + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.destroy(1) } + end + + assert_nothing_raised { Topic.find(1) } + assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) } + end + def test_class_level_delete - should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") - Topic.find(1).replies << should_be_destroyed_reply + should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_not_be_destroyed_reply Topic.delete(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } - assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) } + assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) } + end + + def test_class_level_delete_is_affected_by_scoping + should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_not_be_destroyed_reply + + Topic.where("1=0").scoping { Topic.delete(1) } + assert_nothing_raised { Topic.find(1) } + assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) } end def test_create_with_custom_timestamps @@ -993,44 +1121,35 @@ class PersistenceTest < ActiveRecord::TestCase end class SaveTest < ActiveRecord::TestCase - self.use_transactional_tests = false - def test_save_touch_false - widget = Class.new(ActiveRecord::Base) do - connection.create_table :widgets, force: true do |t| - t.string :name - t.timestamps null: false - end - - self.table_name = :widgets - end - - instance = widget.create!( + pet = Pet.create!( name: "Bob", created_at: 1.day.ago, updated_at: 1.day.ago) - created_at = instance.created_at - updated_at = instance.updated_at + created_at = pet.created_at + updated_at = pet.updated_at - instance.name = "Barb" - instance.save!(touch: false) - assert_equal instance.created_at, created_at - assert_equal instance.updated_at, updated_at - ensure - ActiveRecord::Base.connection.drop_table widget.table_name - widget.reset_column_information + pet.name = "Barb" + pet.save!(touch: false) + assert_equal pet.created_at, created_at + assert_equal pet.updated_at, updated_at end end def test_reset_column_information_resets_children - child = Class.new(Topic) - child.new # force schema to load + child_class = Class.new(Topic) + child_class.new # force schema to load ActiveRecord::Base.connection.add_column(:topics, :foo, :string) Topic.reset_column_information - assert_equal "bar", child.new(foo: :bar).foo + # this should redefine attribute methods + child_class.new + + assert child_class.instance_methods.include?(:foo) + assert child_class.instance_methods.include?(:foo_changed?) + assert_equal "bar", child_class.new(foo: :bar).foo ensure ActiveRecord::Base.connection.remove_column(:topics, :foo) Topic.reset_column_information diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index f1b0d08765..fa7f759e51 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/project" require "timeout" diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 56229b70bc..80016fc19d 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" require "models/topic" @@ -154,10 +156,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_nothing_raised { MixedCaseMonkey.find(1).destroy } end - def test_deprecate_supports_primary_key - assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? } - end - def test_primary_key_returns_value_if_it_exists klass = Class.new(ActiveRecord::Base) do self.table_name = "developers" @@ -300,6 +298,8 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase assert_not column.null assert_equal :string, column.type assert_equal 42, column.limit + ensure + Barcode.reset_column_information end test "schema dump primary key includes type and options" do @@ -324,7 +324,7 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection @connection.schema_cache.clear! - @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + @connection.create_table(:uber_barcodes, primary_key: ["region", "code"], force: true) do |t| t.string :region t.integer :code end @@ -332,14 +332,24 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase t.string :region t.integer :code end + @connection.create_table(:travels, primary_key: ["from", "to"], force: true) do |t| + t.string :from + t.string :to + end end def teardown - @connection.drop_table(:barcodes, if_exists: true) + @connection.drop_table :uber_barcodes, if_exists: true + @connection.drop_table :barcodes_reverse, if_exists: true + @connection.drop_table :travels, if_exists: true end def test_composite_primary_key - assert_equal ["region", "code"], @connection.primary_keys("barcodes") + assert_equal ["region", "code"], @connection.primary_keys("uber_barcodes") + end + + def test_composite_primary_key_with_reserved_words + assert_equal ["from", "to"], @connection.primary_keys("travels") end def test_composite_primary_key_out_of_order @@ -350,7 +360,7 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase def test_primary_key_issues_warning model = Class.new(ActiveRecord::Base) do def self.table_name - "barcodes" + "uber_barcodes" end end warning = capture(:stderr) do @@ -359,9 +369,9 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_match(/WARNING: Active Record does not support composite primary key\./, warning) end - def test_dumping_composite_primary_key - schema = dump_table_schema "barcodes" - assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema + def test_collectly_dump_composite_primary_key + schema = dump_table_schema "uber_barcodes" + assert_match %r{create_table "uber_barcodes", primary_key: \["region", "code"\]}, schema end def test_dumping_composite_primary_key_out_of_order @@ -432,7 +442,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) test "schema dump primary key with serial/integer" do @connection.create_table(:widgets, id: @pk_type, force: true) schema = dump_table_schema "widgets" - assert_match %r{create_table "widgets", id: :#{@pk_type}, force: :cascade}, schema + assert_match %r{create_table "widgets", id: :#{@pk_type}, }, schema end if current_adapter?(:Mysql2Adapter) @@ -445,7 +455,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) assert column.unsigned? schema = dump_table_schema "widgets" - assert_match %r{create_table "widgets", id: :integer, unsigned: true, force: :cascade}, schema + assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema end test "bigint primary key with unsigned" do @@ -457,7 +467,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) assert column.unsigned? schema = dump_table_schema "widgets" - assert_match %r{create_table "widgets", id: :bigint, unsigned: true, force: :cascade}, schema + assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index b018a7b6c0..ad05f70933 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/task" @@ -131,7 +133,7 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off, conn end ensure - ActiveRecord::Base.clear_all_connections! + ActiveRecord::Base.connection_pool.disconnect! end end end @@ -281,7 +283,7 @@ class QueryCacheTest < ActiveRecord::TestCase payload[:sql].downcase! end - assert_raises RuntimeError do + assert_raises frozen_error_class do ActiveRecord::Base.cache do assert_queries(1) { Task.find(1); Task.find(1) } end @@ -300,14 +302,10 @@ class QueryCacheTest < ActiveRecord::TestCase end end - def test_cache_does_not_wrap_string_results_in_arrays + def test_cache_does_not_wrap_results_in_arrays Task.cache do - # Oracle adapter returns count() as Integer or Float - if current_adapter?(:OracleAdapter) - assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) - # Future versions of the sqlite3 adapter will return numeric - assert_instance_of 0.class, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter) + assert_equal 2, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") else assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") end @@ -400,10 +398,8 @@ class QueryCacheTest < ActiveRecord::TestCase # Warm the cache Task.find(1) - Task.connection.type_map.clear - # Preload the type cache again (so we don't have those queries issued during our assertions) - Task.connection.send(:initialize_type_map, Task.connection.type_map) + Task.connection.send(:reload_type_map) # Clear places where type information is cached Task.reset_column_information @@ -456,6 +452,15 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_query_cache_is_enabled_on_all_connection_pools + middleware { + ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| + assert pool.query_cache_enabled + assert pool.connection.query_cache_enabled + end + }.call({}) + end + private def middleware(&app) executor = Class.new(ActiveSupport::Executor) diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 0819776fbf..6534770c57 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -8,11 +10,11 @@ module ActiveRecord end def test_quoted_true - assert_equal "'t'", @quoter.quoted_true + assert_equal "TRUE", @quoter.quoted_true end def test_quoted_false - assert_equal "'f'", @quoter.quoted_false + assert_equal "FALSE", @quoter.quoted_false end def test_quote_column_name @@ -81,23 +83,6 @@ module ActiveRecord end end - class QuotedOne - def quoted_id - 1 - end - end - class SubQuotedOne < QuotedOne - end - def test_quote_with_quoted_id - assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do - assert_equal 1, @quoter.quote(QuotedOne.new) - end - - assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do - assert_equal 1, @quoter.quote(SubQuotedOne.new) - end - end - def test_quote_nil assert_equal "NULL", @quoter.quote(nil) end @@ -126,7 +111,7 @@ module ActiveRecord end def test_quote_bigdecimal - bigdec = BigDecimal.new((1 << 100).to_s) + bigdec = BigDecimal((1 << 100).to_s) assert_equal bigdec.to_s("F"), @quoter.quote(bigdec) end @@ -174,13 +159,21 @@ module ActiveRecord def test_type_cast_date date = Date.today - expected = @conn.quoted_date(date) + if current_adapter?(:Mysql2Adapter) + expected = date + else + expected = @conn.quoted_date(date) + end assert_equal expected, @conn.type_cast(date) end def test_type_cast_time time = Time.now - expected = @conn.quoted_date(time) + if current_adapter?(:Mysql2Adapter) + expected = time + else + expected = @conn.quoted_date(time) + end assert_equal expected, @conn.type_cast(time) end @@ -197,26 +190,6 @@ module ActiveRecord obj = Class.new.new assert_raise(TypeError) { @conn.type_cast(obj) } end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end end class QuoteBooleanTest < ActiveRecord::TestCase @@ -257,7 +230,7 @@ module ActiveRecord def test_type_cast_ar_object value = DatetimePrimaryKey.new(id: @time) - assert_equal "2017-02-14 12:34:56.789000", @connection.type_cast(value) + assert_equal @connection.type_cast(value.id), @connection.type_cast(value) end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 24b678310d..d1b85cb4ef 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/post" diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 249878b67d..6c7727ab1b 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord @@ -16,6 +18,7 @@ module ActiveRecord class FakePool attr_reader :reaped + attr_reader :flushed def initialize @reaped = false @@ -24,6 +27,10 @@ module ActiveRecord def reap @reaped = true end + + def flush + @flushed = true + end end # A reaper with nil time should never reap connections @@ -45,6 +52,7 @@ module ActiveRecord Thread.pass end assert fp.reaped + assert fp.flushed end def test_pool_has_reaper diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c1c2efb9c8..e78c3cee8a 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/customer" @@ -23,7 +25,6 @@ require "models/chef" require "models/department" require "models/cake_designer" require "models/drink_designer" -require "models/mocktail_designer" require "models/recipe" class ReflectionTest < ActiveRecord::TestCase @@ -252,50 +253,35 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end - def test_scope_chain - expected = [ - [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope], - [Post.reflect_on_association(:first_taggings).scope], - [Author.reflect_on_association(:misc_posts).scope] - ] - actual = assert_deprecated do - Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain - end - assert_equal expected, actual - - expected = [ - [ - Tagging.reflect_on_association(:blue_tag).scope, - Post.reflect_on_association(:first_blue_tags_2).scope, - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope - ], - [], - [] - ] - actual = assert_deprecated do - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain - end - assert_equal expected, actual - 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 - assert_equal 1, @hotel.mocktail_designers.size - assert_equal 1, @hotel.mocktail_designers.count - assert_equal 1, @hotel.chef_lists.size + hotel.mocktail_designers = [] + + 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 @@ -335,15 +321,6 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end - def test_association_primary_key_type - # Normal Association - assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type - assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type - - # Through Association - assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type - end - def test_association_primary_key_raises_when_missing_primary_key reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } @@ -364,9 +341,16 @@ class ReflectionTest < ActiveRecord::TestCase assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end + def test_type + assert_equal "taggable_type", Post.reflect_on_association(:taggings).type.to_s + assert_equal "imageable_class", Post.reflect_on_association(:images).type.to_s + assert_nil Post.reflect_on_association(:readers).type + end + def test_foreign_type assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s + assert_nil Sponsor.reflect_on_association(:sponsor_club).foreign_type end def test_collection_association @@ -407,26 +391,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s end - def test_through_reflection_scope_chain_does_not_modify_other_reflections - orig_conds = assert_deprecated do - Post.reflect_on_association(:first_blue_tags_2).scope_chain - end.inspect - assert_deprecated do - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain - end - assert_equal orig_conds, assert_deprecated { - Post.reflect_on_association(:first_blue_tags_2).scope_chain - }.inspect - end - def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end def test_class_for_class_name - assert_deprecated do - assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate? + error = assert_raises(ArgumentError) do + ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm) end + assert_equal "A class was passed to `:class_name` but we are expecting a string.", error.message end def test_join_table diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 3b15f051b2..2696d1bb00 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -8,8 +10,8 @@ module ActiveRecord :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, - :map, :none?, :one?, :partition, :reject, :reverse, - :sample, :second, :sort, :sort_by, :third, + :map, :none?, :one?, :partition, :reject, :reverse, :rotate, + :sample, :second, :sort, :sort_by, :slice, :third, :index, :rindex, :to_ary, :to_set, :to_xml, :to_yaml, :join, :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json ] @@ -24,12 +26,13 @@ module ActiveRecord module DeprecatedArelDelegationTests AREL_METHODS = [ :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql, - :ast, :source, :join_sources, :to_dot, :bind_values, :create_insert, :create_true, :create_false + :ast, :source, :join_sources, :to_dot, :create_insert, :create_true, :create_false ] def test_deprecate_arel_delegation AREL_METHODS.each do |method| assert_deprecated { target.public_send(method) } + assert_deprecated { target.public_send(method) } end end end @@ -38,10 +41,8 @@ module ActiveRecord include DelegationWhitelistTests include DeprecatedArelDelegationTests - fixtures :posts - def target - Post.first.comments + Post.new.comments end end @@ -49,8 +50,6 @@ module ActiveRecord include DelegationWhitelistTests include DeprecatedArelDelegationTests - fixtures :comments - def target Comment.all end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 3901824aac..b68b3723f6 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/comment" @@ -79,13 +81,11 @@ class RelationMergingTest < ActiveRecord::TestCase end test "merge collapses wheres from the LHS only" do - left = Post.where(title: "omg").where(comments_count: 1) + left = Post.where(title: "omg").where(comments_count: 1) right = Post.where(title: "wtf").where(title: "bbq") - expected = [left.bound_attributes[1]] + right.bound_attributes - merged = left.merge(right) + merged = left.merge(right) - assert_equal expected, merged.bound_attributes assert_not_includes merged.to_sql, "omg" assert_includes merged.to_sql, "wtf" assert_includes merged.to_sql, "bbq" diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index dea787c07f..ad3700b73a 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -1,41 +1,10 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" module ActiveRecord class RelationMutationTest < ActiveRecord::TestCase - FakeKlass = Struct.new(:table_name, :name) do - extend ActiveRecord::Delegation::DelegateCache - inherited self - - def connection - Post.connection - end - - def relation_delegate_class(klass) - self.class.relation_delegate_class(klass) - end - - def attribute_alias?(name) - false - end - - def sanitize_sql(sql) - sql - end - - def sanitize_sql_for_order(sql) - sql - end - - def arel_attribute(name, table) - table[name] - end - end - - def relation - @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder - end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) @@ -90,7 +59,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -162,5 +131,15 @@ module ActiveRecord relation.distinct! :foo assert_equal :foo, relation.distinct_value end + + test "skip_query_cache!" do + relation.skip_query_cache! + assert relation.skip_query_cache_value + end + + private + def relation + @relation ||= Relation.new(FakeKlass, Post.arel_table, Post.predicate_builder) + end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 61b6601580..7e418f9c7d 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -1,9 +1,14 @@ +# frozen_string_literal: true + require "cases/helper" +require "models/author" +require "models/categorization" require "models/post" module ActiveRecord class OrTest < ActiveRecord::TestCase fixtures :posts + fixtures :authors, :author_addresses def test_or_with_relation expected = Post.where("id = 1 or id = 2").to_a @@ -26,7 +31,7 @@ module ActiveRecord end def test_or_with_bind_params - assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).to_a + assert_equal Post.find([1, 2]).sort_by(&:id), Post.where(id: 1).or(Post.where(id: 2)).sort_by(&:id) end def test_or_with_null_both @@ -113,5 +118,13 @@ module ActiveRecord Post.where(id: [1, 2, 3]).or(title: "Rails") end end + + def test_or_with_references_inequality + joined = Post.includes(:author) + actual = joined.where(authors: { id: 1 }) + .or(joined.where(title: "I don't have any comments")) + 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 end end diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 48758dc148..b432330deb 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb index 62ca051431..22d32d75bc 100644 --- a/activerecord/test/cases/relation/record_fetch_warning_test.rb +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "active_record/relation/record_fetch_warning" diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 86e150ed79..a68eb2b446 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -25,7 +27,7 @@ module ActiveRecord end def test_association_not_eq - expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(bind_param)) + expected = Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new(1)) relation = Post.joins(:comments).where.not(comments: { title: "hello" }) assert_equal(expected.to_sql, relation.where_clause.ast.to_sql) end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index f8eb0dee91..e5eb159d36 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -1,78 +1,86 @@ +# frozen_string_literal: true + require "cases/helper" class ActiveRecord::Relation class WhereClauseTest < ActiveRecord::TestCase test "+ combines two where clauses" do - first_clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]]) - second_clause = WhereClause.new([table["name"].eq(bind_param)], [["name", "Sean"]]) + first_clause = WhereClause.new([table["id"].eq(bind_param(1))]) + second_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))]) combined = WhereClause.new( - [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [["id", 1], ["name", "Sean"]], + [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))], ) assert_equal combined, first_clause + second_clause end test "+ is associative, but not commutative" do - a = WhereClause.new(["a"], ["bind a"]) - b = WhereClause.new(["b"], ["bind b"]) - c = WhereClause.new(["c"], ["bind c"]) + a = WhereClause.new(["a"]) + b = WhereClause.new(["b"]) + c = WhereClause.new(["c"]) assert_equal a + (b + c), (a + b) + c assert_not_equal a + b, b + a end test "an empty where clause is the identity value for +" do - clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]]) + clause = WhereClause.new([table["id"].eq(bind_param(1))]) assert_equal clause, clause + WhereClause.empty end test "merge combines two where clauses" do - a = WhereClause.new([table["id"].eq(1)], []) - b = WhereClause.new([table["name"].eq("Sean")], []) - expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], []) + a = WhereClause.new([table["id"].eq(1)]) + b = WhereClause.new([table["name"].eq("Sean")]) + expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")]) assert_equal expected, a.merge(b) end test "merge keeps the right side, when two equality clauses reference the same column" do - a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], []) - b = WhereClause.new([table["name"].eq("Jim")], []) - expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")], []) + a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")]) + b = WhereClause.new([table["name"].eq("Jim")]) + expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")]) assert_equal expected, a.merge(b) end test "merge removes bind parameters matching overlapping equality clauses" do a = WhereClause.new( - [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [bind_attribute("id", 1), bind_attribute("name", "Sean")], + [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))], ) b = WhereClause.new( - [table["name"].eq(bind_param)], - [bind_attribute("name", "Jim")] + [table["name"].eq(bind_param("Jim"))], ) expected = WhereClause.new( - [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [bind_attribute("id", 1), bind_attribute("name", "Jim")], + [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Jim"))], ) assert_equal expected, a.merge(b) end test "merge allows for columns with the same name from different tables" do - skip "This is not possible as of 4.2, and the binds do not yet contain sufficient information for this to happen" - # We might be able to change the implementation to remove conflicts by index, rather than column name + table2 = Arel::Table.new("table2") + a = WhereClause.new( + [table["id"].eq(bind_param(1)), table2["id"].eq(bind_param(2))], + ) + b = WhereClause.new( + [table["id"].eq(bind_param(3))], + ) + expected = WhereClause.new( + [table2["id"].eq(bind_param(2)), table["id"].eq(bind_param(3))], + ) + + assert_equal expected, a.merge(b) end test "a clause knows if it is empty" do assert WhereClause.empty.empty? - assert_not WhereClause.new(["anything"], []).empty? + assert_not WhereClause.new(["anything"]).empty? end test "invert cannot handle nil" do - where_clause = WhereClause.new([nil], []) + where_clause = WhereClause.new([nil]) assert_raises ArgumentError do where_clause.invert @@ -86,37 +94,47 @@ class ActiveRecord::Relation table["id"].eq(1), "sql literal", random_object - ], []) + ]) expected = WhereClause.new([ table["id"].not_in([1, 2, 3]), table["id"].not_eq(1), Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")), Arel::Nodes::Not.new(random_object) - ], []) + ]) assert_equal expected, original.invert end - test "accept removes binary predicates referencing a given column" do + test "except removes binary predicates referencing a given column" do where_clause = WhereClause.new([ table["id"].in([1, 2, 3]), - table["name"].eq(bind_param), - table["age"].gteq(bind_param), - ], [ - bind_attribute("name", "Sean"), - bind_attribute("age", 30), + table["name"].eq(bind_param("Sean")), + table["age"].gteq(bind_param(30)), ]) - expected = WhereClause.new([table["age"].gteq(bind_param)], [bind_attribute("age", 30)]) + expected = WhereClause.new([table["age"].gteq(bind_param(30))]) assert_equal expected, where_clause.except("id", "name") end + test "except jumps over unhandled binds (like with OR) correctly" do + wcs = (0..9).map do |i| + WhereClause.new([table["id#{i}"].eq(bind_param(i))]) + end + + wc = wcs[0] + wcs[1] + wcs[2].or(wcs[3]) + wcs[4] + wcs[5] + wcs[6].or(wcs[7]) + wcs[8] + wcs[9] + + expected = wcs[0] + wcs[2].or(wcs[3]) + wcs[5] + wcs[6].or(wcs[7]) + wcs[9] + actual = wc.except("id1", "id2", "id4", "id7", "id8") + + assert_equal expected, actual + end + test "ast groups its predicates with AND" do predicates = [ table["id"].in([1, 2, 3]), - table["name"].eq(bind_param), + table["name"].eq(bind_param(nil)), ] - where_clause = WhereClause.new(predicates, []) + where_clause = WhereClause.new(predicates) expected = Arel::Nodes::And.new(predicates) assert_equal expected, where_clause.ast @@ -128,47 +146,96 @@ class ActiveRecord::Relation table["id"].in([1, 2, 3]), "foo = bar", random_object, - ], []) + ]) expected = Arel::Nodes::And.new([ table["id"].in([1, 2, 3]), Arel::Nodes::Grouping.new(Arel.sql("foo = bar")), - Arel::Nodes::Grouping.new(random_object), + random_object, ]) assert_equal expected, where_clause.ast end test "ast removes any empty strings" do - where_clause = WhereClause.new([table["id"].in([1, 2, 3])], []) - where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ""], []) + where_clause = WhereClause.new([table["id"].in([1, 2, 3])]) + where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ""]) assert_equal where_clause.ast, where_clause_with_empty.ast end test "or joins the two clauses using OR" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) - other_clause = WhereClause.new([table["name"].eq(bind_param)], [bind_attribute("name", "Sean")]) + where_clause = WhereClause.new([table["id"].eq(bind_param(1))]) + other_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))]) expected_ast = Arel::Nodes::Grouping.new( - Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param)) + Arel::Nodes::Or.new(table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))) ) - expected_binds = where_clause.binds + other_clause.binds assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql - assert_equal expected_binds, where_clause.or(other_clause).binds end test "or returns an empty where clause when either side is empty" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) + where_clause = WhereClause.new([table["id"].eq(bind_param(1))]) assert_equal WhereClause.empty, where_clause.or(WhereClause.empty) assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) end + test "or places common conditions before the OR" do + a = WhereClause.new( + [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))], + ) + b = WhereClause.new( + [table["id"].eq(bind_param(1)), table["hair_color"].eq(bind_param("black"))], + ) + + common = WhereClause.new( + [table["id"].eq(bind_param(1))], + ) + + or_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))]) + .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))])) + + assert_equal common + or_clause, a.or(b) + end + + test "or can detect identical or as being a common condition" do + common_or = WhereClause.new([table["name"].eq(bind_param("Sean"))]) + .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))])) + + a = common_or + WhereClause.new([table["id"].eq(bind_param(1))]) + b = common_or + WhereClause.new([table["foo"].eq(bind_param("bar"))]) + + new_or = WhereClause.new([table["id"].eq(bind_param(1))]) + .or(WhereClause.new([table["foo"].eq(bind_param("bar"))])) + + assert_equal common_or + new_or, a.or(b) + end + + test "or will use only common conditions if one side only has common conditions" do + only_common = WhereClause.new([ + table["id"].eq(bind_param(1)), + "foo = bar", + ]) + + common_with_extra = WhereClause.new([ + table["id"].eq(bind_param(1)), + "foo = bar", + table["extra"].eq(bind_param("pluto")), + ]) + + assert_equal only_common, only_common.or(common_with_extra) + assert_equal only_common, common_with_extra.or(only_common) + end + private def table Arel::Table.new("table") end + + def bind_param(value) + Arel::Nodes::BindParam.new(value) + end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 42dae4d569..99797528b2 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/author" require "models/binary" @@ -107,6 +109,15 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_shallow_where_not + treasure = treasures(:sapphire) + + expected = [price_estimates(:diamond), price_estimates(:honda)] + actual = PriceEstimate.where.not(estimate_of: treasure) + + assert_equal expected.sort_by(&:id), actual.sort_by(&:id) + end + def test_polymorphic_nested_array_where treasure = Treasure.new treasure.id = 1 @@ -119,13 +130,23 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_nested_array_where_not + treasure = treasures(:diamond) + car = cars(:honda) + + expected = [price_estimates(:sapphire_1), price_estimates(:sapphire_2)] + actual = PriceEstimate.where.not(estimate_of: [treasure, car]) + + assert_equal expected.sort_by(&:id), actual.sort_by(&:id) + end + def test_polymorphic_array_where_multiple_types treasure_1 = treasures(:diamond) treasure_2 = treasures(:sapphire) car = cars(:honda) expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort - actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort + actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort assert_equal expected, actual end @@ -244,7 +265,7 @@ module ActiveRecord end def test_where_with_decimal_for_string_column - count = Post.where(title: BigDecimal.new(0)).count + count = Post.where(title: BigDecimal(0)).count assert_equal 0, count end @@ -293,6 +314,20 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + def test_where_with_relation_on_has_many_association + essay = essays(:david_modest_proposal) + author = Author.where(essays: Essay.where(id: essay.id)).first + + assert_equal authors(:david), author + end + + def test_where_with_relation_on_has_one_association + author = authors(:david) + author_address = AuthorAddress.where(author: Author.where(id: author.id)).first + assert_equal author_addresses(:david_address), author_address + end + + def test_where_on_association_with_select_relation essay = Essay.where(author: Author.where(name: "David").select(:name)).take assert_equal essays(:david_modest_proposal), essay diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index a403824f1a..dbf3389774 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -8,24 +10,6 @@ module ActiveRecord class RelationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :author_addresses, :ratings - FakeKlass = Struct.new(:table_name, :name) do - extend ActiveRecord::Delegation::DelegateCache - - inherited self - - def self.connection - Post.connection - end - - def self.table_name - "fake_table" - end - - def self.sanitize_sql_for_order(sql) - sql - end - end - def test_construction relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass @@ -40,10 +24,9 @@ module ActiveRecord def test_initialize_single_values relation = Relation.new(FakeKlass, :b, nil) - (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal false, relation.readonly_value value = relation.create_with_value assert_equal({}, value) assert_predicate value, :frozen? @@ -70,8 +53,8 @@ module ActiveRecord def test_has_values relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - relation.where! relation.table[:id].eq(10) - assert_equal({ id: 10 }, relation.where_values_hash) + relation.where!(id: 10) + assert_equal({ "id" => 10 }, relation.where_values_hash) end def test_values_wrong_table @@ -84,16 +67,11 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) - combine = left.and right + combine = left.or(right) relation.where! combine assert_equal({}, relation.where_values_hash) end - def test_table_name_delegates_to_klass - relation = Relation.new(FakeKlass.new("posts"), :b, Post.predicate_builder) - assert_equal "posts", relation.table_name - end - def test_scope_for_create relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) @@ -101,28 +79,27 @@ module ActiveRecord def test_create_with_value relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - hash = { hello: "world" } - relation.create_with_value = hash - assert_equal hash, relation.scope_for_create + 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.where! relation.table[:id].eq(10) + assert_equal({}, relation.scope_for_create) + + relation.where!(id: 10) + assert_equal({ "id" => 10 }, relation.scope_for_create) + relation.create_with_value = { hello: "world" } - assert_equal({ hello: "world", id: 10 }, relation.scope_for_create) + assert_equal({ "hello" => "world", "id" => 10 }, relation.scope_for_create) end - # FIXME: is this really wanted or expected behavior? - def test_scope_for_create_is_cached + def test_empty_scope relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - assert_equal({}, relation.scope_for_create) + assert relation.empty_scope? - relation.where! relation.table[:id].eq(10) - assert_equal({}, relation.scope_for_create) - - relation.create_with_value = { hello: "world" } - assert_equal({}, relation.scope_for_create) + relation.merge!(relation) + assert relation.empty_scope? end def test_bad_constants_raise_errors @@ -210,7 +187,7 @@ module ActiveRecord relation = Relation.new(klass, :b, nil) relation.merge!(where: ["foo = ?", "bar"]) - assert_equal Relation::WhereClause.new(["foo = bar"], []), relation.where_clause + assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause end def test_merging_readonly_false @@ -296,18 +273,32 @@ module ActiveRecord assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) end + def test_relation_merging_keeps_joining_order + authors = Author.where(id: 1) + posts = Post.joins(:author).merge(authors) + comments = Comment.joins(:post).merge(posts) + ratings = Rating.joins(:comment).merge(comments) + + assert_equal 3, ratings.count + end + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value def type :string end + def cast(value) + raise value unless value == "value from user" + "cast value" + end + def deserialize(value) raise value unless value == "type cast for database" "type cast from database" end def serialize(value) - raise value unless value == "value from user" + raise value unless value == "cast value" "type cast for database" end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5767dec315..7785f8c99b 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/tag" require "models/tagging" @@ -15,15 +17,14 @@ require "models/car" require "models/engine" require "models/tyre" require "models/minivan" -require "models/aircraft" require "models/possession" require "models/reader" +require "models/category" require "models/categorization" require "models/edge" class RelationTest < ActiveRecord::TestCase - fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, - :tags, :taggings, :cars, :minivans + fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans class TopicWithCallbacks < ActiveRecord::Base self.table_name = :topics @@ -194,6 +195,18 @@ class RelationTest < ActiveRecord::TestCase assert_equal(relation.map(&:post_count).sort, subquery.values.sort) end + def test_finding_with_subquery_with_eager_loading_in_from + relation = Comment.includes(:post).where("posts.type": "Post") + assert_equal relation.to_a, Comment.select("*").from(relation).to_a + assert_equal relation.to_a, Comment.select("subquery.*").from(relation).to_a + assert_equal relation.to_a, Comment.select("a.*").from(relation, :a).to_a + end + + def test_finding_with_subquery_with_eager_loading_in_where + relation = Comment.includes(:post).where("posts.type": "Post") + assert_equal relation.sort_by(&:id), Comment.where(id: relation).sort_by(&:id) + end + def test_finding_with_conditions assert_equal ["David"], Author.where(name: "David").map(&:name) assert_equal ["Mary"], Author.where(["name = ?", "Mary"]).map(&:name) @@ -237,7 +250,7 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function - topics = Topic.order("length(title)").reverse_order + topics = Topic.order(Arel.sql("length(title)")).reverse_order assert_equal topics(:second).title, topics.first.title end @@ -247,24 +260,24 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function_other_predicates - topics = Topic.order("author_name, length(title), id").reverse_order + topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order assert_equal topics(:second).title, topics.first.title - topics = Topic.order("length(author_name), id, length(title)").reverse_order + topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order assert_equal topics(:fifth).title, topics.first.title end def test_reverse_order_with_multiargument_function assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(author_name, title)").reverse_order + Topic.order(Arel.sql("concat(author_name, title)")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(lower(author_name), title)").reverse_order + Topic.order(Arel.sql("concat(lower(author_name), title)")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(author_name, lower(title))").reverse_order + Topic.order(Arel.sql("concat(author_name, lower(title))")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(lower(author_name), title, length(title)").reverse_order + Topic.order(Arel.sql("concat(lower(author_name), title, length(title)")).reverse_order end end @@ -276,10 +289,10 @@ class RelationTest < ActiveRecord::TestCase def test_reverse_order_with_nulls_first_or_last assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("title NULLS FIRST").reverse_order + Topic.order(Arel.sql("title NULLS FIRST")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("title nulls last").reverse_order + Topic.order(Arel.sql("title nulls last")).reverse_order end end @@ -372,29 +385,29 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_cross_table_order_and_limit tags = Tag.includes(:taggings). - order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)"). + order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")). limit(1).to_a assert_equal 1, tags.length end def test_finding_with_complex_order_and_limit - tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a + tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a assert_equal 1, tags.length end def test_finding_with_complex_order - tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a + tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a assert_equal 3, tags.length end def test_finding_with_sanitized_order - query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql assert_match(/field\(id, 1,3,2\)/, query) - query = Tag.order(["field(id, ?)", []]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql assert_match(/field\(id, NULL\)/, query) - query = Tag.order(["field(id, ?)", nil]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql assert_match(/field\(id, NULL\)/, query) end @@ -420,123 +433,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal [2, 4, 6, 8, 10], even_ids.sort end - def test_none - assert_no_queries(ignore_none: false) do - assert_equal [], Developer.none - assert_equal [], Developer.all.none - end - end - - def test_none_chainable - assert_no_queries(ignore_none: false) do - assert_equal [], Developer.none.where(name: "David") - end - end - - def test_none_chainable_to_existing_scope_extension_method - assert_no_queries(ignore_none: false) do - assert_equal 1, Topic.anonymous_extension.none.one - end - end - - def test_none_chained_to_methods_firing_queries_straight_to_db - assert_no_queries(ignore_none: false) do - assert_equal [], Developer.none.pluck(:id, :name) - assert_equal 0, Developer.none.delete_all - assert_equal 0, Developer.none.update_all(name: "David") - assert_equal 0, Developer.none.delete(1) - assert_equal false, Developer.none.exists?(1) - end - end - - def test_null_relation_content_size_methods - assert_no_queries(ignore_none: false) do - assert_equal 0, Developer.none.size - assert_equal 0, Developer.none.count - assert_equal true, Developer.none.empty? - assert_equal true, Developer.none.none? - assert_equal false, Developer.none.any? - assert_equal false, Developer.none.one? - assert_equal false, Developer.none.many? - end - end - - def test_null_relation_calculations_methods - assert_no_queries(ignore_none: false) do - assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil) - assert_nil Developer.none.calculate(:average, "salary") - end - end - - def test_null_relation_metadata_methods - assert_equal "", Developer.none.to_sql - assert_equal({}, Developer.none.where_values_hash) - end - - def test_null_relation_where_values_hash - assert_equal({ "salary" => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) - end - - def test_null_relation_sum - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count - ac.save - assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count - end - - def test_null_relation_count - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:id).count - assert_equal 0, ac.engines.count - ac.save - assert_equal Hash.new, ac.engines.group(:id).count - assert_equal 0, ac.engines.count - end - - def test_null_relation_size - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:id).size - assert_equal 0, ac.engines.size - ac.save - assert_equal Hash.new, ac.engines.group(:id).size - assert_equal 0, ac.engines.size - end - - def test_null_relation_average - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_nil ac.engines.average(:id) - ac.save - assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_nil ac.engines.average(:id) - end - - def test_null_relation_minimum - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) - assert_nil ac.engines.minimum(:id) - ac.save - assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) - assert_nil ac.engines.minimum(:id) - end - - def test_null_relation_maximum - ac = Aircraft.new - assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) - assert_nil ac.engines.maximum(:id) - ac.save - assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) - assert_nil ac.engines.maximum(:id) - end - - def test_null_relation_in_where_condition - assert_operator Comment.count, :>, 0 # precondition, make sure there are comments. - assert_equal 0, Comment.where(post_id: Post.none).to_a.size - end - def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -586,14 +482,6 @@ class RelationTest < ActiveRecord::TestCase assert_nothing_raised { Topic.reorder([]) } end - def test_scoped_responds_to_delegated_methods - relation = Topic.all - - ["map", "uniq", "sort", "insert", "delete", "update"].each do |method| - assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" - end - end - def test_respond_to_delegates_to_arel relation = Topic.all fake_arel = Struct.new(:responds) { @@ -892,8 +780,6 @@ class RelationTest < ActiveRecord::TestCase def test_find_all_using_where_with_relation david = authors(:david) - # switching the lines below would succeed in current rails - # assert_queries(2) { assert_queries(1) { relation = Author.where(id: Author.where(id: david.id)) assert_equal [david], relation.to_a @@ -932,8 +818,6 @@ class RelationTest < ActiveRecord::TestCase def test_find_all_using_where_with_relation_and_alternate_primary_key cool_first = minivans(:cool_first) - # switching the lines below would succeed in current rails - # assert_queries(2) { assert_queries(1) { relation = Minivan.where(minivan_id: Minivan.where(name: cool_first.name)) assert_equal [cool_first], relation.to_a @@ -969,32 +853,6 @@ class RelationTest < ActiveRecord::TestCase } end - def test_exists - davids = Author.where(name: "David") - assert davids.exists? - assert davids.exists?(authors(:david).id) - assert ! davids.exists?(authors(:mary).id) - assert ! davids.exists?("42") - assert ! davids.exists?(42) - assert ! davids.exists?(davids.new.id) - - fake = Author.where(name: "fake author") - assert ! fake.exists? - assert ! fake.exists?(authors(:david).id) - end - - def test_exists_uses_existing_scope - post = authors(:david).posts.first - authors = Author.includes(:posts).where(name: "David", posts: { id: post.id }) - assert authors.exists?(authors(:david).id) - end - - def test_any_with_scope_on_hash_includes - post = authors(:david).posts.first - categories = Categorization.includes(author: :posts).where(posts: { id: post.id }) - assert categories.exists? - end - def test_last authors = Author.all assert_equal authors(:bob), authors.last @@ -1034,11 +892,9 @@ class RelationTest < ActiveRecord::TestCase end def test_delete_all_with_unpermitted_relation_raises_error - assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } end def test_select_with_aggregates @@ -1077,13 +933,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.count(:all) assert_equal 11, posts.count(:id) - assert_equal 1, posts.where("comments_count > 1").count - assert_equal 9, posts.where(comments_count: 0).count + assert_equal 3, posts.where("comments_count > 1").count + assert_equal 6, posts.where(comments_count: 0).count end def test_count_with_block posts = Post.all - assert_equal 10, posts.count { |p| p.comments_count.even? } + assert_equal 8, posts.count { |p| p.comments_count.even? } end def test_count_on_association_relation @@ -1100,13 +956,19 @@ class RelationTest < ActiveRecord::TestCase def test_count_with_distinct posts = Post.all - assert_equal 3, posts.distinct(true).count(:comments_count) + assert_equal 4, posts.distinct(true).count(:comments_count) assert_equal 11, posts.distinct(false).count(:comments_count) - assert_equal 3, posts.distinct(true).select(:comments_count).count + assert_equal 4, posts.distinct(true).select(:comments_count).count assert_equal 11, posts.distinct(false).select(:comments_count).count end + def test_size_with_distinct + posts = Post.distinct.select(:author_id, :comments_count) + assert_queries(1) { assert_equal 8, posts.size } + assert_queries(1) { assert_equal 8, posts.load.size } + end + def test_update_all_with_scope tag = Tag.first Post.tagged_with(tag.id).update_all title: "rofl" @@ -1142,7 +1004,7 @@ class RelationTest < ActiveRecord::TestCase best_posts = posts.where(comments_count: 0) best_posts.load # force load - assert_no_queries { assert_equal 9, best_posts.size } + assert_no_queries { assert_equal 6, best_posts.size } end def test_size_with_limit @@ -1153,7 +1015,7 @@ class RelationTest < ActiveRecord::TestCase best_posts = posts.where(comments_count: 0) best_posts.load # force load - assert_no_queries { assert_equal 9, best_posts.size } + assert_no_queries { assert_equal 6, best_posts.size } end def test_size_with_zero_limit @@ -1176,7 +1038,7 @@ class RelationTest < ActiveRecord::TestCase def test_count_complex_chained_relations posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0") - expected = { 1 => 2 } + expected = { 1 => 4, 2 => 1 } assert_equal expected, posts.count end @@ -1327,6 +1189,15 @@ class RelationTest < ActiveRecord::TestCase assert_equal "hen", hen.name end + def test_create_with_polymorphic_association + author = authors(:david) + post = posts(:welcome) + comment = Comment.where(post: post, author: author).create!(body: "hello") + + assert_equal author, comment.author + assert_equal post, comment.post + end + def test_first_or_create parrot = Bird.where(color: "green").first_or_create(name: "parrot") assert_kind_of Bird, parrot @@ -1482,7 +1353,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal bird, Bird.find_or_initialize_by(name: "bob") end - def test_explicit_create_scope + def test_explicit_create_with hens = Bird.where(name: "hen") assert_equal "hen", hens.new.name @@ -1717,8 +1588,12 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body") assert_equal ["comments"], scope.references_values - scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") - assert_equal ["comments"], scope.references_values + scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) + if current_adapter?(:OracleAdapter) + assert_equal ["COMMENTS"], scope.references_values + else + assert_equal ["comments"], scope.references_values + end scope = Post.order("comments.body", "yaks.body") assert_equal ["comments", "yaks"], scope.references_values @@ -1730,7 +1605,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body asc") assert_equal ["comments"], scope.references_values - scope = Post.order("foo(comments.body)") + scope = Post.order(Arel.sql("foo(comments.body)")) assert_equal [], scope.references_values end @@ -1738,8 +1613,12 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body") assert_equal %w(comments), scope.references_values - scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") - assert_equal ["comments"], scope.references_values + scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) + if current_adapter?(:OracleAdapter) + assert_equal ["COMMENTS"], scope.references_values + else + assert_equal ["comments"], scope.references_values + end scope = Post.reorder("comments.body", "yaks.body") assert_equal %w(comments yaks), scope.references_values @@ -1751,7 +1630,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body asc") assert_equal %w(comments), scope.references_values - scope = Post.reorder("foo(comments.body)") + scope = Post.reorder(Arel.sql("foo(comments.body)")) assert_equal [], scope.references_values end @@ -1877,7 +1756,7 @@ class RelationTest < ActiveRecord::TestCase test "relations with cached arel can't be mutated [internal API]" do relation = Post.all - relation.count + relation.arel assert_raises(ActiveRecord::ImmutableRelation) { relation.limit!(5) } assert_raises(ActiveRecord::ImmutableRelation) { relation.where!("1 = 2") } @@ -1911,15 +1790,23 @@ class RelationTest < ActiveRecord::TestCase end test "using a custom table affects the wheres" do - table_alias = Post.arel_table.alias("omg_posts") + post = posts(:welcome) + + assert_equal post, custom_post_relation.where!(title: post.title).take + end + + test "using a custom table with joins affects the joins" do + post = posts(:welcome) - table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) - predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) - relation.where!(foo: "bar") + assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take + end + + test "arel_attribute respects a custom table" do + assert_equal [posts(:sti_comments)], custom_post_relation.ranked_by_comments.limit_by(1).to_a + end - node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first - assert_equal table_alias, node.relation + test "alias_tracker respects a custom table" do + assert_equal posts(:welcome), custom_post_relation("categories_posts").joins(:categories).first end test "#load" do @@ -1976,37 +1863,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, posts.unscope(where: :body).count end - def test_unscope_removes_binds - left = Post.where(id: 20) - - binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] - assert_equal binds, left.bound_attributes - - relation = left.unscope(where: :id) - assert_equal [], relation.bound_attributes - end - - def test_merging_removes_rhs_binds - left = Post.where(id: 20) - right = Post.where(id: [1, 2, 3, 4]) - - binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] - assert_equal binds, left.bound_attributes - - merged = left.merge(right) - assert_equal [], merged.bound_attributes - end - - def test_merging_keeps_lhs_binds - binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] - - right = Post.where(id: 20) - left = Post.where(id: 10) - - merged = left.merge(right) - assert_equal binds, merged.bound_attributes - end - def test_locked_should_not_build_arel posts = Post.locked assert posts.locked? @@ -2017,21 +1873,67 @@ class RelationTest < ActiveRecord::TestCase assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end - def test_connection_adapters_can_reorder_binds - posts = Post.limit(1).offset(2) + test "#skip_query_cache!" do + Post.cache do + assert_queries(1) do + Post.all.load + Post.all.load + end + + assert_queries(2) do + Post.all.skip_query_cache!.load + Post.all.skip_query_cache!.load + end + end + end + + test "#skip_query_cache! with an eager load" do + Post.cache do + assert_queries(1) do + Post.eager_load(:comments).load + Post.eager_load(:comments).load + end - stubbed_connection = Post.connection.dup - def stubbed_connection.combine_bind_parameters(**kwargs) - offset = kwargs[:offset] - kwargs[:offset] = kwargs[:limit] - kwargs[:limit] = offset - super(**kwargs) + assert_queries(2) do + Post.eager_load(:comments).skip_query_cache!.load + Post.eager_load(:comments).skip_query_cache!.load + end end + end + + test "#skip_query_cache! with a preload" do + Post.cache do + assert_queries(2) do + Post.preload(:comments).load + Post.preload(:comments).load + end - posts.define_singleton_method(:connection) do - stubbed_connection + assert_queries(4) do + Post.preload(:comments).skip_query_cache!.load + Post.preload(:comments).skip_query_cache!.load + end end + end - assert_equal 2, posts.to_a.length + test "#where with set" do + david = authors(:david) + mary = authors(:mary) + + authors = Author.where(name: ["David", "Mary"].to_set) + assert_equal [david, mary], authors + end + + test "#where with empty set" do + authors = Author.where(name: Set.new) + assert_empty authors end + + private + def custom_post_relation(alias_name = "omg_posts") + table_alias = Post.arel_table.alias(alias_name) + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + + ActiveRecord::Relation.create(Post, table_alias, predicate_builder) + end end diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 3f4c0c03e3..72f4bfaf6d 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + require "cases/helper" require "models/owner" require "models/pet" class ReloadModelsTest < ActiveRecord::TestCase + include ActiveSupport::Testing::Isolation + fixtures :pets, :owners def test_has_one_with_reload @@ -19,4 +23,4 @@ class ReloadModelsTest < ActiveRecord::TestCase pet.owner = Owner.find_by_name("ashley") assert_equal pet.owner, Owner.find_by_name("ashley") end -end +end unless in_memory_db? diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb index f3019a5326..4f8ca392b9 100644 --- a/activerecord/test/cases/reserved_word_test.rb +++ b/activerecord/test/cases/reserved_word_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class ReservedWordTest < ActiveRecord::TestCase @@ -37,7 +39,7 @@ class ReservedWordTest < ActiveRecord::TestCase t.string :order t.belongs_to :select end - @connection.create_table :values, force: true do |t| + @connection.create_table :values, primary_key: :as, force: true do |t| t.belongs_to :group end end @@ -86,6 +88,13 @@ class ReservedWordTest < ActiveRecord::TestCase assert_equal x, Group.find(x.id) end + def test_delete_all_with_subselect + create_test_fixtures :values + assert_equal 1, Values.order(:as).limit(1).offset(1).delete_all + assert_raise(ActiveRecord::RecordNotFound) { Values.find(2) } + assert Values.find(1) + end + def test_has_one_associations create_test_fixtures :group, :values v = Group.find(1).values diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 1a0b7c6ca7..db52c108ac 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 72f09186e2..1b0605e369 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/binary" require "models/author" @@ -9,30 +11,30 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") - assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"]) - assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars]) + assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"]) + assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") - assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"]) - assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"]) + assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_named_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"]) - assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"]) + assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) end def test_sanitize_sql_array_handles_relations @@ -41,42 +43,50 @@ class SanitizeTest < ActiveRecord::TestCase sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i - select_author_sql = Post.send(:sanitize_sql_array, ["id in (?)", david_posts]) + select_author_sql = Post.sanitize_sql_array(["id in (?)", david_posts]) assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for bind variables") - select_author_sql = Post.send(:sanitize_sql_array, ["id in (:post_ids)", post_ids: david_posts]) + select_author_sql = Post.sanitize_sql_array(["id in (:post_ids)", post_ids: david_posts]) assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for named bind variables") end def test_sanitize_sql_array_handles_empty_statement - select_author_sql = Post.send(:sanitize_sql_array, [""]) + select_author_sql = Post.sanitize_sql_array([""]) assert_equal("", select_author_sql) end def test_sanitize_sql_like - assert_equal '100\%', Binary.send(:sanitize_sql_like, "100%") - assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, "snake_cased_string") - assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint') - assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42") + assert_equal '100\%', Binary.sanitize_sql_like("100%") + assert_equal 'snake\_cased\_string', Binary.sanitize_sql_like("snake_cased_string") + assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint') + assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42") end def test_sanitize_sql_like_with_custom_escape_character - assert_equal "100!%", Binary.send(:sanitize_sql_like, "100%", "!") - assert_equal "snake!_cased!_string", Binary.send(:sanitize_sql_like, "snake_cased_string", "!") - assert_equal "great!!", Binary.send(:sanitize_sql_like, "great!", "!") - assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', "!") - assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42", "!") + assert_equal "100!%", Binary.sanitize_sql_like("100%", "!") + assert_equal "snake!_cased!_string", Binary.sanitize_sql_like("snake_cased_string", "!") + assert_equal "great!!", Binary.sanitize_sql_like("great!", "!") + assert_equal 'C:\\Programs\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint', "!") + assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42", "!") end def test_sanitize_sql_like_example_use_case searchable_post = Class.new(Post) do - def self.search(term) + def self.search_as_method(term) where("title LIKE ?", sanitize_sql_like(term, "!")) end + + scope :search_as_scope, -> (term) { + where("title LIKE ?", sanitize_sql_like(term, "!")) + } + end + + assert_sql(/LIKE '20!% !_reduction!_!!'/) do + searchable_post.search_as_method("20% _reduction_!").to_a end assert_sql(/LIKE '20!% !_reduction!_!!'/) do - searchable_post.search("20% _reduction_!").to_a + searchable_post.search_as_scope("20% _reduction_!").to_a end end @@ -151,18 +161,6 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", bind("name=?", "Bambi\nand\nThumper".mb_chars) end - def test_bind_record - o = Class.new { - def quoted_id - 1 - end - }.new - assert_deprecated { assert_equal "1", bind("?", o) } - - os = [o] * 3 - assert_deprecated { assert_equal "1,1,1", bind("?", os) } - end - def test_named_bind_with_postgresql_type_casts l = Proc.new { bind(":a::integer '2009-01-01'::date", a: "10") } assert_nothing_raised(&l) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 4c81e825fa..a612ce9bb2 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -64,7 +66,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_uses_force_cascade_on_create_table output = dump_table_schema "authors" - assert_match %r{create_table "authors", force: :cascade}, output + assert_match %r{create_table "authors",.* force: :cascade}, output end def test_schema_dump_excludes_sqlite_sequence @@ -175,14 +177,14 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition - elsif current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) if ActiveRecord::Base.connection.supports_index_sort_order? assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition end + elsif ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end @@ -197,6 +199,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dumps_index_sort_order + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip + if ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition + else + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition + end + end + + def test_schema_dumps_index_length + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip + if current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition + else + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition + end + end + def test_schema_dump_should_honor_nonstandard_primary_keys output = standard_dump match = output.match(%r{create_table "movies"(.*)do}) @@ -205,20 +225,25 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dump_should_use_false_as_default - output = standard_dump + output = dump_table_schema "booleans" assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output end def test_schema_dump_does_not_include_limit_for_text_field - output = standard_dump + output = dump_table_schema "admin_users" assert_match %r{t\.text\s+"params"$}, output end def test_schema_dump_does_not_include_limit_for_binary_field - output = standard_dump + output = dump_table_schema "binaries" assert_match %r{t\.binary\s+"data"$}, output end + def test_schema_dump_does_not_include_limit_for_float_field + output = dump_table_schema "numeric_data" + assert_match %r{t\.float\s+"temperature"$}, output + end + if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields output = standard_dump @@ -287,20 +312,32 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.oid\s+"obj_id"$}, output end - if ActiveRecord::Base.connection.supports_extensions? - def test_schema_dump_includes_extensions - connection = ActiveRecord::Base.connection + 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.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.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 - 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 + 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.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 end end @@ -384,6 +421,31 @@ class SchemaDumperTest < ActiveRecord::TestCase $stdout = original end + def test_schema_dump_with_table_name_prefix_and_suffix_regexp_escape + original, $stdout = $stdout, StringIO.new + ActiveRecord::Base.table_name_prefix = "foo$" + ActiveRecord::Base.table_name_suffix = "$bar" + + migration = CreateDogMigration.new + migration.migrate(:up) + + output = perform_schema_dump + assert_no_match %r{create_table "foo\$.+\$bar"}, output + assert_no_match %r{add_index "foo\$.+\$bar"}, output + assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo\$.+\$bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo\$.+\$bar"}, output + end + ensure + migration.migrate(:down) + + ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = "" + $stdout = original + end + def test_schema_dump_with_table_name_prefix_and_ignoring_tables original, $stdout = $stdout, StringIO.new diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb index 362370ac61..f539156466 100644 --- a/activerecord/test/cases/schema_loading_test.rb +++ b/activerecord/test/cases/schema_loading_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module SchemaLoadCounter diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 89fb434b27..fdfeabaa3b 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/comment" @@ -118,49 +120,49 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_with_where_attributes expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(name: "David").unscope(where: :name).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort expected_2 = Developer.order("salary DESC").collect(&:name) received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name) - assert_equal expected_2, received_2 + assert_equal expected_2.sort, received_2.sort expected_3 = Developer.order("salary DESC").collect(&:name) received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name) - assert_equal expected_3, received_3 + assert_equal expected_3.sort, received_3.sort expected_4 = Developer.order("salary DESC").collect(&:name) received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name) - assert_equal expected_4, received_4 + assert_equal expected_4.sort, received_4.sort expected_5 = Developer.order("salary DESC").collect(&:name) received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) - assert_equal expected_5, received_5 + assert_equal expected_5.sort, received_5.sort expected_6 = Developer.order("salary DESC").collect(&:name) received_6 = DeveloperOrderedBySalary.where(Developer.arel_table["name"].eq("David")).unscope(where: :name).collect(&:name) - assert_equal expected_6, received_6 + assert_equal expected_6.sort, received_6.sort expected_7 = Developer.order("salary DESC").collect(&:name) received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq("David")).unscope(where: :name).collect(&:name) - assert_equal expected_7, received_7 + assert_equal expected_7.sort, received_7.sort end def test_unscope_comparison_where_clauses # unscoped for WHERE (`developers`.`id` <= 2) expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name } - assert_equal expected, received + assert_equal expected.sort, received.sort # unscoped for WHERE (`developers`.`id` < 2) expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name } - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_multiple_where_clauses expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(name: "Jamis").where(id: 1).unscope(where: [:name, :id]).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_string_where_clauses_involved @@ -170,23 +172,23 @@ class DefaultScopingTest < ActiveRecord::TestCase dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at > ?", 1.year.ago) received = dev_ordered_relation.unscope(where: [:name]).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_with_grouping_attributes expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort expected_2 = Developer.order("salary DESC").collect(&:name) received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name) - assert_equal expected_2, received_2 + assert_equal expected_2.sort, received_2.sort end def test_unscope_with_limit_in_query expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_order_to_unscope_reordering @@ -332,7 +334,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_create_with_merge aaron = PoorDeveloperCalledJamis.create_with(name: "foo", salary: 20).merge( - PoorDeveloperCalledJamis.create_with(name: "Aaron")).new + PoorDeveloperCalledJamis.create_with(name: "Aaron")).new assert_equal 20, aaron.salary assert_equal "Aaron", aaron.name @@ -342,6 +344,11 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Aaron", aaron.name end + def test_create_with_using_both_string_and_symbol + jamis = PoorDeveloperCalledJamis.create_with(name: "foo").create_with("name" => "Aaron").new + assert_equal "Aaron", jamis.name + end + def test_create_with_reset jamis = PoorDeveloperCalledJamis.create_with(name: "Aaron").create_with(nil).new assert_equal "Jamis", jamis.name @@ -372,11 +379,39 @@ class DefaultScopingTest < ActiveRecord::TestCase Comment.joins(:post).count end + def test_joins_not_affected_by_scope_other_than_default_or_unscoped + without_scope_on_post = Comment.joins(:post).to_a + with_scope_on_post = nil + Post.where(id: [1, 5, 6]).scoping do + with_scope_on_post = Comment.joins(:post).to_a + end + + assert_equal with_scope_on_post, without_scope_on_post + end + def test_unscoped_with_joins_should_not_have_default_scope assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, Comment.joins(:post).to_a end + def test_sti_association_with_unscoped_not_affected_by_default_scope + post = posts(:thinking) + comments = [comments(:does_it_hurt)] + + post.special_comments.update_all(deleted_at: Time.now) + + assert_raises(ActiveRecord::RecordNotFound) { Post.joins(:special_comments).find(post.id) } + assert_equal [], post.special_comments + + SpecialComment.unscoped do + assert_equal post, Post.joins(:special_comments).find(post.id) + assert_equal comments, Post.joins(:special_comments).find(post.id).special_comments + assert_equal comments, Post.eager_load(:special_comments).find(post.id).special_comments + assert_equal comments, Post.includes(:special_comments).find(post.id).special_comments + assert_equal comments, Post.preload(:special_comments).find(post.id).special_comments + end + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end @@ -437,7 +472,7 @@ class DefaultScopingTest < ActiveRecord::TestCase test "a scope can remove the condition from the default scope" do scope = DeveloperCalledJamis.david2 assert_equal 1, scope.where_clause.ast.children.length - assert_equal Developer.where(name: "David"), scope + assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id) end def test_with_abstract_class_where_clause_should_not_be_duplicated @@ -486,6 +521,8 @@ class DefaultScopingWithThreadTest < ActiveRecord::TestCase end def test_default_scope_is_threadsafe + 2.times { ThreadsafeDeveloper.unscoped.create! } + threads = [] assert_not_equal 1, ThreadsafeDeveloper.unscoped.count @@ -504,5 +541,7 @@ class DefaultScopingWithThreadTest < ActiveRecord::TestCase ThreadsafeDeveloper.connection.close end threads.each(&:join) + ensure + ThreadsafeDeveloper.unscoped.destroy_all end end unless in_memory_db? diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 483ea7128d..17d3f27bb1 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/topic" @@ -115,7 +117,8 @@ class NamedScopingTest < ActiveRecord::TestCase assert_not_equal Post.containing_the_letter_a, authors(:david).posts assert !Post.containing_the_letter_a.empty? - assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.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) end def test_scope_with_STI @@ -127,7 +130,8 @@ class NamedScopingTest < ActiveRecord::TestCase assert_not_equal Comment.containing_the_letter_e, authors(:david).comments assert !Comment.containing_the_letter_e.empty? - assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.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 @@ -147,6 +151,22 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "The scope body needs to be callable.", e.message end + def test_scopes_name_is_relation_method + conflicts = [ + :records, + :to_ary, + :to_sql, + :explain + ] + + conflicts.each do |name| + e = assert_raises ArgumentError do + Class.new(Post).class_eval { scope name, -> { where(approved: true) } } + end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) + end + end + def test_active_records_have_scope_named__all__ assert !Topic.all.empty? diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 10553bf057..116f8e83aa 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/post" require "models/author" @@ -238,6 +240,20 @@ class RelationScopingTest < ActiveRecord::TestCase assert_nil SpecialComment.current_scope end + def test_scoping_respects_current_class + Comment.unscoped do + assert_equal "a comment...", Comment.all.what_are_you + assert_equal "a special comment...", SpecialComment.all.what_are_you + end + end + + def test_scoping_respects_sti_constraint + Comment.unscoped do + assert_equal comments(:greetings), Comment.find(1) + assert_raises(ActiveRecord::RecordNotFound) { SpecialComment.find(1) } + end + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb index 7b9cbee40a..f5fa6aa302 100644 --- a/activerecord/test/cases/secure_token_test.rb +++ b/activerecord/test/cases/secure_token_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/user" diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index ec33ad38f2..2d829ad4ba 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/contact" require "models/topic" diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index e1bdaab5cf..32dafbd458 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index fab3648564..ad6cd198e2 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/book" require "models/liquid" @@ -10,7 +12,6 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end - #Cache v 1.1 tests def test_statement_cache Book.create(name: "my book") Book.create(name: "my other book") @@ -19,9 +20,9 @@ module ActiveRecord Book.where(name: params.bind) end - b = cache.execute([ "my book" ], Book, Book.connection) + b = cache.execute([ "my book" ], Book.connection) assert_equal "my book", b[0].name - b = cache.execute([ "my other book" ], Book, Book.connection) + b = cache.execute([ "my other book" ], Book.connection) assert_equal "my other book", b[0].name end @@ -33,9 +34,9 @@ module ActiveRecord Book.where(id: params.bind) end - b = cache.execute([ b1.id ], Book, Book.connection) + b = cache.execute([ b1.id ], Book.connection) assert_equal b1.name, b[0].name - b = cache.execute([ b2.id ], Book, Book.connection) + b = cache.execute([ b2.id ], Book.connection) assert_equal b2.name, b[0].name end @@ -49,8 +50,6 @@ module ActiveRecord assert_equal("my other book", b.name) end - #End - def test_statement_cache_with_simple_statement cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book").where("author_id > 3") @@ -58,7 +57,7 @@ module ActiveRecord Book.create(name: "my book", author_id: 4) - books = cache.execute([], Book, Book.connection) + books = cache.execute([], Book.connection) assert_equal "my book", books[0].name end @@ -71,7 +70,7 @@ module ActiveRecord molecule = salty.molecules.create(name: "dioxane") molecule.electrons.create(name: "lepton") - liquids = cache.execute([], Book, Book.connection) + liquids = cache.execute([], Book.connection) assert_equal "salty", liquids[0].name end @@ -84,13 +83,13 @@ module ActiveRecord Book.create(name: "my book") end - first_books = cache.execute([], Book, Book.connection) + first_books = cache.execute([], Book.connection) 3.times do Book.create(name: "my book") end - additional_books = cache.execute([], Book, Book.connection) + additional_books = cache.execute([], Book.connection) assert first_books != additional_books end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 633a8a0ebc..ebf4016960 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/admin" require "models/admin/user" diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index a7d16b7cdb..b68f0033d9 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/notification" require "models/user" diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index c47c97e9d9..c114842dec 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/tasks/database_tasks" @@ -28,13 +30,30 @@ module ActiveRecord def test_raises_an_error_when_called_with_protected_environment ActiveRecord::Migrator.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments.dup + protected_environments = ActiveRecord::Base.protected_environments + current_env = ActiveRecord::Migrator.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 + ensure + ActiveRecord::Base.protected_environments = protected_environments + end + + def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Migrator.current_environment assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - ActiveRecord::Base.protected_environments << current_env + ActiveRecord::Base.protected_environments = [current_env.to_sym] assert_raise(ActiveRecord::ProtectedEnvironmentError) do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end @@ -91,6 +110,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path) assert File.file?(path) ensure + ActiveRecord::Base.clear_cache! FileUtils.rm_rf(path) end end @@ -355,8 +375,15 @@ module ActiveRecord ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) ActiveRecord::Tasks::DatabaseTasks.migrate + 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 + ENV["VERBOSE"] = "yes" - ENV["VERSION"] = "unknown" + 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) @@ -365,15 +392,47 @@ module ActiveRecord ENV["VERBOSE"], ENV["VERSION"] = verbose, version end - def test_migrate_raise_error_on_empty_version + def test_migrate_raise_error_on_invalid_version_format version = ENV["VERSION"] - ENV["VERSION"] = "" + + ENV["VERSION"] = "unknown" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "0.1.11" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1.1.11" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "0 " + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1." e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } - assert_equal "Empty VERSION provided", e.message + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1_" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1_name" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_match(/Invalid format of target version/, e.message) ensure ENV["VERSION"] = version 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 + end + def test_migrate_clears_schema_cache_afterward ActiveRecord::Base.expects(:clear_cache!) ActiveRecord::Tasks::DatabaseTasks.migrate @@ -442,6 +501,108 @@ module ActiveRecord end end + class DatabaseTaskTargetVersionTest < ActiveRecord::TestCase + def test_target_version_returns_nil_if_version_does_not_exist + version = ENV.delete("VERSION") + assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version + ensure + ENV["VERSION"] = version + end + + def test_target_version_returns_nil_if_version_is_empty + version = ENV["VERSION"] + + ENV["VERSION"] = "" + assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version + ensure + ENV["VERSION"] = version + end + + def test_target_version_returns_converted_to_integer_env_version_if_version_exists + version = ENV["VERSION"] + + ENV["VERSION"] = "0" + assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version + + ENV["VERSION"] = "42" + assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version + + ENV["VERSION"] = "042" + assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version + ensure + ENV["VERSION"] = version + end + end + + class DatabaseTaskCheckTargetVersionTest < ActiveRecord::TestCase + def test_check_target_version_does_not_raise_error_on_empty_version + version = ENV["VERSION"] + ENV["VERSION"] = "" + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + ensure + ENV["VERSION"] = version + end + + def test_check_target_version_does_not_raise_error_if_version_is_not_setted + version = ENV.delete("VERSION") + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + ensure + ENV["VERSION"] = version + end + + def test_check_target_version_raises_error_on_invalid_version_format + version = ENV["VERSION"] + + ENV["VERSION"] = "unknown" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "0.1.11" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1.1.11" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "0 " + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1." + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1_" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + + ENV["VERSION"] = "1_name" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + assert_match(/Invalid format of target version/, e.message) + ensure + ENV["VERSION"] = version + end + + def test_check_target_version_does_not_raise_error_on_valid_version_format + version = ENV["VERSION"] + + ENV["VERSION"] = "0" + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + + ENV["VERSION"] = "1" + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + + ENV["VERSION"] = "001" + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + + ENV["VERSION"] = "001_name.rb" + assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version } + ensure + ENV["VERSION"] = version + end + end + class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 9c6fb14376..047153e7cc 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/tasks/database_tasks" @@ -73,7 +75,7 @@ if current_adapter?(:Mysql2Adapter) end end - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase def setup @connection = stub("Connection", create_database: true) @error = Mysql2::Error.new("Invalid permissions") @@ -84,13 +86,8 @@ if current_adapter?(:Mysql2Adapter) "password" => "wossname" } - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) + ActiveRecord::Base.stubs(:establish_connection).raises(@error) $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr @@ -100,75 +97,11 @@ if current_adapter?(:Mysql2Adapter) $stdout, $stderr = @original_stdout, @original_stderr end - def test_root_password_is_requested - assert_permissions_granted_for("pat") - $stdin.expects(:gets).returns("secret\n") - - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - def test_connection_established_as_root - assert_permissions_granted_for("pat") - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "mysql2", - "database" => nil, - "username" => "root", - "password" => "secret" - ) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - def test_database_created_by_root - assert_permissions_granted_for("pat") - @connection.expects(:create_database). - with("my-app-db", {}) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - def test_grant_privileges_for_normal_user - assert_permissions_granted_for("pat") - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - def test_do_not_grant_privileges_for_root_user - @configuration["username"] = "root" - @configuration["password"] = "" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - def test_connection_established_as_normal_user - assert_permissions_granted_for("pat") - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "mysql2", - "database" => "my-app-db", - "username" => "pat", - "password" => "secret" - ) - - raise @error + def test_raises_error + assert_raises(Mysql2::Error) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration end - - ActiveRecord::Tasks::DatabaseTasks.create @configuration end - - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) - - $stderr.expects(:puts).at_least_once.returns(nil) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - - private - - def assert_permissions_granted_for(db_user) - db_name = @configuration["database"] - db_password = @configuration["password"] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end end class MySQLDBDropTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index a2e968aedf..ca1defa332 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/tasks/database_tasks" @@ -227,7 +229,6 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def teardown @@ -293,6 +294,16 @@ if current_adapter?(:PostgreSQLAdapter) 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) + end + assert_match("failed to execute:", e.message) + end + private def with_dump_schemas(value, &block) old_dump_schemas = ActiveRecord::Base.dump_schemas @@ -321,7 +332,6 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def test_structure_load diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index ccb3834fee..d7e3caa2ff 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_record/tasks/database_tasks" require "pathname" @@ -213,6 +215,31 @@ if current_adapter?(:SQLite3Adapter) FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) end + + 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") } + end + end + assert_match("failed to execute:", e.message) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + + private + def with_structure_dump_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old + end end class SqliteStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 9f594fef85..024b5bd8a1 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/test_case" require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" @@ -76,12 +78,8 @@ module ActiveRecord model.column_names.include?(column_name.to_s) end - def bind_param - Arel::Nodes::BindParam.new - end - - def bind_attribute(name, value, type = ActiveRecord::Type.default_value) - ActiveRecord::Relation::QueryAttribute.new(name, value, type) + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError end end @@ -116,9 +114,9 @@ module ActiveRecord # FIXME: this needs to be refactored so specific database can add their own # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. - oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im] mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 58d3bea3a2..4411410eda 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class TestFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 09c585167e..41455637bb 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 39b40e3411..54e3f47e16 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/ddl_helper" require "models/developer" @@ -88,7 +90,7 @@ class TimestampTest < ActiveRecord::TestCase @developer.touch(:created_at) end - assert !@developer.created_at_changed? , "created_at should not be changed" + assert !@developer.created_at_changed?, "created_at should not be changed" assert !@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 @@ -444,17 +446,6 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal ["created_at", "updated_at"], toy.send(:all_timestamp_attributes_in_model) end - - def test_index_is_created_for_both_timestamps - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, index: true - end - - indexes = ActiveRecord::Base.connection.indexes("foos") - assert_equal ["created_at", "updated_at"], indexes.flat_map(&:columns).sort - ensure - ActiveRecord::Base.connection.drop_table(:foos) - end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase @@ -473,4 +464,15 @@ class TimestampsWithoutTransactionTest < ActiveRecord::TestCase assert_nil post.updated_at end end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, index: true + end + + indexes = ActiveRecord::Base.connection.indexes("foos") + assert_equal ["created_at", "updated_at"], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index d1e8c649d9..1757031371 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/invoice" require "models/line_item" diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index eaa4dd09a9..1f370a80ee 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/owner" require "models/pet" diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index 58abfadaf4..eaafd13360 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" unless ActiveRecord::Base.connection.supports_transaction_isolation? @@ -13,9 +15,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation? end end end -end - -if ActiveRecord::Base.connection.supports_transaction_isolation? +else class TransactionIsolationTest < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 76a997ba8b..c110fa2f7d 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" @@ -574,7 +576,7 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_saving_a_frozen_record topic = Topic.new(title: "test") topic.freeze - e = assert_raise(RuntimeError) { topic.save } + e = assert_raise(frozen_error_class) { topic.save } # Not good enough, but we can't do much # about it since there is no specific error # for frozen objects. @@ -684,7 +686,7 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - assert_nil movie.id + assert_nil movie.movieid end def test_assign_id_after_rollback @@ -707,8 +709,54 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - movie.id = nil - assert_nil movie.id + movie.movieid = nil + assert_nil movie.movieid + end + + def test_read_attribute_after_rollback + topic = Topic.new + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.read_attribute(:id) + end + + def test_read_attribute_with_custom_primary_key_after_rollback + movie = Movie.new(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + assert_nil movie.read_attribute(:movieid) + end + + def test_write_attribute_after_rollback + topic = Topic.create! + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + topic.write_attribute(:id, nil) + assert_nil topic.id + end + + def test_write_attribute_with_custom_primary_key_after_rollback + movie = Movie.create!(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + movie.write_attribute(:movieid, nil) + assert_nil movie.movieid end def test_rollback_of_frozen_records @@ -906,27 +954,25 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase end end if Topic.connection.supports_savepoints? -if current_adapter?(:PostgreSQLAdapter) +if ActiveRecord::Base.connection.supports_transaction_isolation? class ConcurrentTransactionTest < TransactionTest # This will cause transactions to overlap and fail unless they are performed on # separate database connections. - unless in_memory_db? - def test_transaction_per_thread - threads = 3.times.map do - Thread.new do - Topic.transaction do - topic = Topic.find(1) - topic.approved = !topic.approved? - assert topic.save! - topic.approved = !topic.approved? - assert topic.save! - end - Topic.connection.close + def test_transaction_per_thread + threads = 3.times.map do + Thread.new do + Topic.transaction do + topic = Topic.find(1) + topic.approved = !topic.approved? + assert topic.save! + topic.approved = !topic.approved? + assert topic.save! end + Topic.connection.close end - - threads.each(&:join) end + + threads.each(&:join) end # Test for dirty reads among simultaneous transactions. diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb index 8b836b4793..b58bdd5549 100644 --- a/activerecord/test/cases/type/adapter_specific_registry_test.rb +++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb index 6848619ece..c9558e25b5 100644 --- a/activerecord/test/cases/type/date_time_test.rb +++ b/activerecord/test/cases/type/date_time_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/task" diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 368b6d7199..15d1a675a1 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/company" diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index a95da864fa..8c51b30fdd 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb index 2959d36466..f3699c11a2 100644 --- a/activerecord/test/cases/type/type_map_test.rb +++ b/activerecord/test/cases/type/type_map_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb index 1cd4dbc2c5..dd05cf3fff 100644 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb index d45a9b3141..93ae563c8b 100644 --- a/activerecord/test/cases/type_test.rb +++ b/activerecord/test/cases/type_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class TypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 11476ea0ef..3f7fb0a604 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index b210584644..f4d8be5897 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class TestRecord < ActiveRecord::Base diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb new file mode 100644 index 0000000000..72d4997d0b --- /dev/null +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" +require "models/comment" + +class UnsafeRawSqlTest < ActiveRecord::TestCase + fixtures :posts, :comments + + test "order: allows string column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows symbol column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:title).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:title).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows downcase symbol direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :asc).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :asc).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows upcase symbol direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("ASC")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :ASC).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :ASC).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows string direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: "asc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: "asc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows multiple columns" do + ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, :title).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, :title).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows mixed" do + ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, title: :asc).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, title: :asc).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows table and column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows column name and direction in string" do + ids_expected = Post.order(Arel.sql("title desc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title desc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title desc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows table name, column name and direction in string" do + ids_expected = Post.order(Arel.sql("title desc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title desc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title desc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: disallows invalid column name" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order("len(title) asc").pluck(:id) + end + end + end + + test "order: disallows invalid direction" do + with_unsafe_raw_sql_disabled do + assert_raises(ArgumentError) do + Post.order(title: :foo).pluck(:id) + end + end + end + + test "order: disallows invalid column with direction" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order("len(title)" => :asc).pluck(:id) + end + end + end + + test "order: always allows Arel" do + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("length(title)")).pluck(:title) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("length(title)")).pluck(:title) } + + assert_equal ids_depr, ids_disabled + end + + test "order: allows Arel.sql with binds" do + ids_expected = Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz'), id")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: disallows invalid bind statement" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order(["REPLACE(title, ?, ?), id", "misc", "zzzz"]).pluck(:id) + end + end + end + + test "order: disallows invalid Array arguments" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order(["author_id", "length(title)"]).pluck(:id) + end + end + end + + test "order: allows valid Array arguments" do + ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: logs deprecation warning for unrecognized column" do + with_unsafe_raw_sql_deprecated do + assert_deprecated(/Dangerous query method/) do + Post.order("length(title)") + end + end + end + + test "pluck: allows string column name" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("title") } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("title") } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: allows symbol column name" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title) } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title) } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: allows multiple column names" do + values_expected = Post.pluck(Arel.sql("title"), Arel.sql("id")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title, :id) } + values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title, :id) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows column names with includes" do + values_expected = Post.includes(:comments).pluck(Arel.sql("title"), Arel.sql("id")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, :id) } + values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, :id) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows auto-generated attributes" do + values_expected = Post.pluck(Arel.sql("tags_count")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:tags_count) } + values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:tags_count) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows table and column names" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("posts.title") } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("posts.title") } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: disallows invalid column name" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.pluck("length(title)") + end + end + end + + test "pluck: disallows invalid column name amongst valid names" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.pluck(:title, "length(title)") + end + end + end + + test "pluck: disallows invalid column names with includes" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.includes(:comments).pluck(:title, "length(title)") + end + end + end + + test "pluck: always allows Arel" do + values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) } + values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) } + + assert_equal values_depr, values_disabled + end + + test "pluck: logs deprecation warning" do + with_unsafe_raw_sql_deprecated do + assert_deprecated(/Dangerous query method/) do + Post.includes(:comments).pluck(:title, "length(title)") + end + end + end + + def with_unsafe_raw_sql_disabled(&blk) + with_config(:disabled, &blk) + end + + def with_unsafe_raw_sql_deprecated(&blk) + with_config(:deprecated, &blk) + end + + def with_config(new_value, &blk) + old_value = ActiveRecord::Base.allow_unsafe_raw_sql + ActiveRecord::Base.allow_unsafe_raw_sql = new_value + blk.call + ensure + ActiveRecord::Base.allow_unsafe_raw_sql = old_value + end +end diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index 870619e4e7..a997f8be9c 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/face" require "models/interest" diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index f5ceb27d97..80fe375ae5 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index a57065ba75..703c24b340 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index fd88a3ea67..b7c52ea18c 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index ba45c6dcc1..87ce4c6f37 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/owner" require "models/pet" diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 13956e26ec..3ab1567b51 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/man" require "models/face" diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 28605d2f8e..a10567f066 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" @@ -154,6 +156,13 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r3.valid?, "Saving r3" end + def test_validate_uniqueness_with_scope_invalid_syntax + error = assert_raises(ArgumentError) do + Reply.validates_uniqueness_of(:content, scope: { parent_id: false }) + end + assert_match(/Pass a symbol or an array of symbols instead/, error.to_s) + end + def test_validate_uniqueness_with_object_scope Reply.validates_uniqueness_of(:content, scope: :topic) diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index b30666d876..6dc3b64b2b 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ValidationsRepairHelper extend ActiveSupport::Concern diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a305aa295a..14623c43d2 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" @@ -173,12 +175,12 @@ class ValidationsTest < ActiveRecord::TestCase ActiveModel::Name.new(self, nil, "Topic") end attribute :wibble, :decimal, scale: 2, precision: 9 - validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + 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.new("97.179")).valid? + assert_not klass.new(wibble: BigDecimal("97.179")).valid? end def test_acceptance_validator_doesnt_require_db_connection diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 1d21a2454f..7e2d66c62a 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/book" require "support/schema_dumping_helper" diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index dd8d2c1178..578881f754 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/reply" diff --git a/activerecord/test/config.rb b/activerecord/test/config.rb index a65e6ff776..72cdfb16ef 100644 --- a/activerecord/test/config.rb +++ b/activerecord/test/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + TEST_ROOT = __dir__ ASSETS_ROOT = TEST_ROOT + "/assets" FIXTURES_ROOT = TEST_ROOT + "/fixtures" diff --git a/activerecord/test/fixtures/binaries.yml b/activerecord/test/fixtures/binaries.yml index ec8f2facdc..53b7883369 100644 --- a/activerecord/test/fixtures/binaries.yml +++ b/activerecord/test/fixtures/binaries.yml @@ -131,3 +131,7 @@ flowers: SgCUASgCUASgCUASgAC74PbXOTvE5/En7jpSoLE8/wBn7uPJjKyj46T9D/NT pKsXyQzxNpdNP0/akB5484WkMKh4RfXG4UafNmH7b0UxWMrb7Nxg6rl9Z/Im w+vWq0iscQwxQroiUIvkKsRZQBKAJQBKAJQB/9k= + +binary_helper: + id: 2 + data: <%= binary(ASSETS_ROOT + "/flowers.jpg") %> diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml index 39ff763547..3e11a33802 100644 --- a/activerecord/test/fixtures/other_posts.yml +++ b/activerecord/test/fixtures/other_posts.yml @@ -5,3 +5,4 @@ second_welcome: author_id: 1 title: Welcome to the another weblog body: It's really nice today + comments_count: 1 diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 86d46f753a..8d7e1e0ae7 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -28,6 +28,7 @@ sti_comments: author_id: 1 title: sti comments body: hello + comments_count: 5 type: Post sti_post_and_comments: @@ -35,6 +36,7 @@ sti_post_and_comments: author_id: 1 title: sti me body: hello + comments_count: 2 type: StiPost sti_habtm: @@ -50,6 +52,8 @@ eager_other: title: eager loading with OR'd conditions body: hello type: Post + comments_count: 1 + tags_count: 3 misc_by_bob: id: 8 @@ -57,6 +61,7 @@ misc_by_bob: title: misc post by bob body: hello type: Post + tags_count: 1 misc_by_mary: id: 9 @@ -64,6 +69,7 @@ misc_by_mary: title: misc post by mary body: hello type: Post + tags_count: 1 other_by_bob: id: 10 @@ -71,6 +77,7 @@ other_by_bob: title: other post by bob body: hello type: Post + tags_count: 1 other_by_mary: id: 11 @@ -78,3 +85,4 @@ other_by_mary: title: other post by mary body: hello type: Post + tags_count: 1 diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml index 7d109609ab..9ed9e5edc5 100644 --- a/activerecord/test/fixtures/reserved_words/values.yml +++ b/activerecord/test/fixtures/reserved_words/values.yml @@ -1,7 +1,7 @@ values1: - id: 1 + as: 1 group_id: 2 values2: - id: 2 + as: 2 group_id: 1 diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb index e908c9eabc..4b0d5fb6fa 100644 --- a/activerecord/test/migrations/10_urban/9_add_expressions.rb +++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddExpressions < ActiveRecord::Migration::Current def self.up create_table("expressions") do |t| 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 43c79bc20b..b892f50e41 100644 --- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb +++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GiveMeBigNumbers < ActiveRecord::Migration::Current def self.up create_table :big_numbers do |table| diff --git a/activerecord/test/migrations/empty/.gitkeep b/activerecord/test/migrations/empty/.keep index e69de29bb2..e69de29bb2 100644 --- a/activerecord/test/migrations/empty/.gitkeep +++ b/activerecord/test/migrations/empty/.keep diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb index e046944e31..d3c9b127fb 100644 --- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb +++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveMiddleNames < ActiveRecord::Migration::Current def self.up add_column "people", "middle_name", :string diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb index 50fe2a9c8e..bd5f5ea11e 100644 --- a/activerecord/test/migrations/missing/1_people_have_last_names.rb +++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb index d7c63ac892..4647268c6e 100644 --- a/activerecord/test/migrations/missing/3_we_need_reminders.rb +++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb index bd3bf21576..8063bc0558 100644 --- a/activerecord/test/migrations/missing/4_innocent_jointable.rb +++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", id: false) do |t| diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb index 9dce01acfd..8e71a1d996 100644 --- a/activerecord/test/migrations/rename/1_we_need_things.rb +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WeNeedThings < ActiveRecord::Migration::Current def self.up create_table("things") do |t| diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb index cb8484e7dc..110fe3f0fa 100644 --- a/activerecord/test/migrations/rename/2_rename_things.rb +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RenameThings < ActiveRecord::Migration::Current def self.up rename_table "things", "awesome_things" diff --git a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb index 76734bcd7d..badccf65cc 100644 --- a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveHobbies < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text diff --git a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb index 7f883dbb45..1d19d5d6f4 100644 --- a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveDescriptions < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text diff --git a/activerecord/test/migrations/to_copy2/1_create_articles.rb b/activerecord/test/migrations/to_copy2/1_create_articles.rb index 2e9f5ec6bc..85c166b319 100644 --- a/activerecord/test/migrations/to_copy2/1_create_articles.rb +++ b/activerecord/test/migrations/to_copy2/1_create_articles.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy2/2_create_comments.rb b/activerecord/test/migrations/to_copy2/2_create_comments.rb index d361847d4b..1d213a1705 100644 --- a/activerecord/test/migrations/to_copy2/2_create_comments.rb +++ b/activerecord/test/migrations/to_copy2/2_create_comments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateComments < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb index 1a863367dd..d9fef596f5 100644 --- a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveHobbies < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :string diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb index 76734bcd7d..badccf65cc 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveHobbies < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb index 7f883dbb45..1d19d5d6f4 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeopleHaveDescriptions < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb index 2e9f5ec6bc..85c166b319 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb index d361847d4b..1d213a1705 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateComments < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb index c450211d8c..3bedcdcdf0 100644 --- a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb index d7c63ac892..4647268c6e 100644 --- a/activerecord/test/migrations/valid/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb index bd3bf21576..8063bc0558 100644 --- a/activerecord/test/migrations/valid/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", id: false) do |t| diff --git a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb index c450211d8c..3bedcdcdf0 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb index d7c63ac892..4647268c6e 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb index bd3bf21576..8063bc0558 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", id: false) do |t| diff --git a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb index 9fd27593f0..b938847170 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string diff --git a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb index 4a59921136..94551e8208 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| diff --git a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb index be24de6d70..672edc5253 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", id: false) do |t| diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb index 6f314c881c..91bfbbdfd1 100644 --- a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb +++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MigrationVersionCheck < ActiveRecord::Migration::Current def self.up raise "incorrect migration version" unless version == 20131219224947 diff --git a/activerecord/test/models/account.rb b/activerecord/test/models/account.rb new file mode 100644 index 0000000000..0c3cd45a81 --- /dev/null +++ b/activerecord/test/models/account.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Account < ActiveRecord::Base + belongs_to :firm, class_name: "Company" + belongs_to :unautosaved_firm, foreign_key: "firm_id", class_name: "Firm", autosave: false + + alias_attribute :available_credit, :credit_limit + + def self.destroyed_account_ids + @destroyed_account_ids ||= Hash.new { |h, k| h[k] = [] } + end + + # Test private kernel method through collection proxy using has_many. + def self.open + where("firm_name = ?", "37signals") + end + + before_destroy do |account| + if account.firm + Account.destroyed_account_ids[account.firm.id] << account.id + end + end + + validate :check_empty_credit_limit + + private + def check_empty_credit_limit + errors.add("credit_limit", :blank) if credit_limit.blank? + end + + def private_method + "Sir, yes sir!" + end +end diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb index bc3ce23447..a40b5a33b2 100644 --- a/activerecord/test/models/admin.rb +++ b/activerecord/test/models/admin.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Admin def self.table_name_prefix "admin_" diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb index bd23192d20..41fe2d782b 100644 --- a/activerecord/test/models/admin/account.rb +++ b/activerecord/test/models/admin/account.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::Account < ActiveRecord::Base has_many :users end diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb index b64ae7fc41..d89b8dd293 100644 --- a/activerecord/test/models/admin/randomly_named_c1.rb +++ b/activerecord/test/models/admin/randomly_named_c1.rb @@ -1,7 +1,9 @@ -class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
- self.table_name = :randomly_named_table2
-end
-
-class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
- self.table_name = :randomly_named_table3
-end
+# frozen_string_literal: true + +class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base + self.table_name = :randomly_named_table2 +end + +class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base + self.table_name = :randomly_named_table3 +end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index a76e4b6795..abb5cb28e7 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::User < ActiveRecord::Base class Coder def initialize(default = {}) diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb index ebd42ff824..4fdea46cf7 100644 --- a/activerecord/test/models/aircraft.rb +++ b/activerecord/test/models/aircraft.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Aircraft < ActiveRecord::Base self.pluralize_table_names = false has_many :engines, foreign_key: "car_id" diff --git a/activerecord/test/models/arunit2_model.rb b/activerecord/test/models/arunit2_model.rb index 04b8b15d3d..5b0da8a249 100644 --- a/activerecord/test/models/arunit2_model.rb +++ b/activerecord/test/models/arunit2_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ARUnit2Model < ActiveRecord::Base self.abstract_class = true end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 2d9cba77e0..cb8686f315 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Author < ActiveRecord::Base has_many :posts has_many :serialized_posts @@ -19,7 +21,8 @@ class Author < ActiveRecord::Base end has_many :comments_containing_the_letter_e, through: :posts, source: :comments has_many :comments_with_order_and_conditions, -> { order("comments.body").where("comments.body like 'Thank%'") }, through: :posts, source: :comments - has_many :comments_with_include, -> { includes(:post) }, through: :posts, source: :comments + has_many :comments_with_include, -> { includes(:post).where(posts: { type: "Post" }) }, through: :posts, source: :comments + has_many :comments_for_first_author, -> { for_first_author }, through: :posts, source: :comments has_many :first_posts has_many :comments_on_first_posts, -> { order("posts.id desc, comments.id asc") }, through: :first_posts, source: :comments @@ -38,6 +41,7 @@ class Author < ActiveRecord::Base class_name: "Post" has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, 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 has_many :ordered_uniq_comments_desc, -> { distinct.order("comments.id DESC") }, through: :posts, source: :comments @@ -76,7 +80,7 @@ class Author < ActiveRecord::Base after_add: [:log_after_adding, Proc.new { |o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}" }] has_many :unchangeable_posts, class_name: "Post", before_add: :raise_exception, after_add: :log_after_adding - has_many :categorizations + has_many :categorizations, -> {} has_many :categories, through: :categorizations has_many :named_categories, through: :categorizations @@ -97,10 +101,12 @@ class Author < ActiveRecord::Base has_many :taggings, through: :posts, source: :taggings has_many :taggings_2, through: :posts, source: :tagging has_many :tags, through: :posts + has_many :ordered_tags, through: :posts has_many :post_categories, through: :posts, source: :categories has_many :tagging_tags, through: :taggings, source: :tag has_many :similar_posts, -> { distinct }, through: :tags, source: :tagged_posts + has_many :ordered_posts, -> { distinct }, through: :ordered_tags, source: :tagged_posts has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, through: :posts, source: :tags has_many :tags_with_primary_key, through: :posts diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb index 82c6544bd5..fd672603bb 100644 --- a/activerecord/test/models/auto_id.rb +++ b/activerecord/test/models/auto_id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AutoId < ActiveRecord::Base self.table_name = "auto_id_tests" self.primary_key = "auto_id" diff --git a/activerecord/test/models/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb index 5578ba0d9b..c46e34c101 100644 --- a/activerecord/test/models/autoloadable/extra_firm.rb +++ b/activerecord/test/models/autoloadable/extra_firm.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ExtraFirm < Company end diff --git a/activerecord/test/models/binary.rb b/activerecord/test/models/binary.rb index 39b2f5090a..b93f87519f 100644 --- a/activerecord/test/models/binary.rb +++ b/activerecord/test/models/binary.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Binary < ActiveRecord::Base end diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb index 24b839135d..be08636ac6 100644 --- a/activerecord/test/models/bird.rb +++ b/activerecord/test/models/bird.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Bird < ActiveRecord::Base belongs_to :pirate validates_presence_of :name diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 6466e1b341..afdda1a81e 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Book < ActiveRecord::Base belongs_to :author diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb index 0da228aac2..bee757fb9c 100644 --- a/activerecord/test/models/boolean.rb +++ b/activerecord/test/models/boolean.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Boolean < ActiveRecord::Base def has_fun super diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 113d21cb84..ab92f7025d 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Bulb < ActiveRecord::Base default_scope { where(name: "defaulty") } belongs_to :car, touch: true diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb index 9c57ef573a..0b2a00edfd 100644 --- a/activerecord/test/models/cake_designer.rb +++ b/activerecord/test/models/cake_designer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CakeDesigner < ActiveRecord::Base has_one :chef, as: :employable end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 92bff7ff96..3d6a7a96c2 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Car < ActiveRecord::Base has_many :bulbs has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb" diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb index 230be118c3..995a9d3bef 100644 --- a/activerecord/test/models/carrier.rb +++ b/activerecord/test/models/carrier.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Carrier < ActiveRecord::Base end diff --git a/activerecord/test/models/cat.rb b/activerecord/test/models/cat.rb index dfdde18641..43013964b6 100644 --- a/activerecord/test/models/cat.rb +++ b/activerecord/test/models/cat.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Cat < ActiveRecord::Base self.abstract_class = true diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index b99383d0b1..68b0ea90d3 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Categorization < ActiveRecord::Base belongs_to :post belongs_to :category, counter_cache: true diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 4b2840c653..2ccc00bed9 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Category < ActiveRecord::Base has_and_belongs_to_many :posts has_and_belongs_to_many :special_posts, class_name: "Post" diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb index 9d3dd01016..ff528644bc 100644 --- a/activerecord/test/models/chef.rb +++ b/activerecord/test/models/chef.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Chef < ActiveRecord::Base belongs_to :employable, polymorphic: true has_many :recipes diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb index 7d06387f56..3d786f27eb 100644 --- a/activerecord/test/models/citation.rb +++ b/activerecord/test/models/citation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Citation < ActiveRecord::Base belongs_to :reference_of, class_name: "Book", foreign_key: :book2_id end diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 3d441b1d48..2006e05fcf 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Club < ActiveRecord::Base has_one :membership has_many :memberships, inverse_of: false diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb index c9dbe1ecc2..52017dda42 100644 --- a/activerecord/test/models/college.rb +++ b/activerecord/test/models/college.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_dependency "models/arunit2_model" require "active_support/core_ext/object/with_options" diff --git a/activerecord/test/models/column.rb b/activerecord/test/models/column.rb index 499358b4cf..d3cd419a00 100644 --- a/activerecord/test/models/column.rb +++ b/activerecord/test/models/column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Column < ActiveRecord::Base belongs_to :record end diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb index 460eb4fe20..c6047c507b 100644 --- a/activerecord/test/models/column_name.rb +++ b/activerecord/test/models/column_name.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ColumnName < ActiveRecord::Base self.table_name = "colnametests" end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index eecf923046..5ab433f2d9 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,3 +1,8 @@ +# frozen_string_literal: true + +# `counter_cache` requires association class before `attr_readonly`. +class Post < ActiveRecord::Base; end + class Comment < ActiveRecord::Base scope :limit_by, lambda { |l| limit(l) } scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") } @@ -54,6 +59,11 @@ class Comment < ActiveRecord::Base end class SpecialComment < Comment + default_scope { where(deleted_at: nil) } + + def self.what_are_you + "a special comment..." + end end class SubSpecialComment < SpecialComment @@ -74,3 +84,9 @@ class CommentWithDefaultScopeReferencesAssociation < Comment default_scope -> { includes(:developer).order("developers.name").references(:developer) } belongs_to :developer end + +class CommentWithAfterCreateUpdate < Comment + after_create do + update_attributes(body: "bar") + end +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index c6a5bf1c92..fc6488f729 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbstractCompany < ActiveRecord::Base self.abstract_class = true end @@ -85,6 +87,8 @@ class Firm < Company has_many :association_with_references, -> { references(:foo) }, class_name: "Client" + has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer" + has_one :lead_developer, class_name: "Developer" has_many :projects @@ -185,37 +189,4 @@ end class VerySpecialClient < SpecialClient end -class Account < ActiveRecord::Base - belongs_to :firm, class_name: "Company" - belongs_to :unautosaved_firm, foreign_key: "firm_id", class_name: "Firm", autosave: false - - alias_attribute :available_credit, :credit_limit - - def self.destroyed_account_ids - @destroyed_account_ids ||= Hash.new { |h, k| h[k] = [] } - end - - # Test private kernel method through collection proxy using has_many. - def self.open - where("firm_name = ?", "37signals") - end - - before_destroy do |account| - if account.firm - Account.destroyed_account_ids[account.firm.id] << account.id - end - true - end - - validate :check_empty_credit_limit - - private - - def check_empty_credit_limit - errors.add("credit_limit", :blank) if credit_limit.blank? - end - - def private_method - "Sir, yes sir!" - end -end +require "models/account" diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 0782c1eff4..52b7e06a63 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/object/with_options" module MyApplication diff --git a/activerecord/test/models/computer.rb b/activerecord/test/models/computer.rb index 1c9856e1af..582b4a38b5 100644 --- a/activerecord/test/models/computer.rb +++ b/activerecord/test/models/computer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Computer < ActiveRecord::Base belongs_to :developer, foreign_key: "developer" end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 47bbbbfd8b..6e02ff199b 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ContactFakeColumns def self.extended(base) base.class_eval do diff --git a/activerecord/test/models/content.rb b/activerecord/test/models/content.rb index 68db2127d8..14bbee53d8 100644 --- a/activerecord/test/models/content.rb +++ b/activerecord/test/models/content.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Content < ActiveRecord::Base self.table_name = "content" has_one :content_position, dependent: :destroy diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index 32bd581377..f273badd85 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class Contract < ActiveRecord::Base belongs_to :company - belongs_to :developer + belongs_to :developer, primary_key: :id belongs_to :firm, foreign_key: "company_id" before_save :hi diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb index 7912719ddd..0c84a40de2 100644 --- a/activerecord/test/models/country.rb +++ b/activerecord/test/models/country.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Country < ActiveRecord::Base self.primary_key = :country_id diff --git a/activerecord/test/models/course.rb b/activerecord/test/models/course.rb index 348f2bf1e0..4f346124ea 100644 --- a/activerecord/test/models/course.rb +++ b/activerecord/test/models/course.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_dependency "models/arunit2_model" class Course < ARUnit2Model diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 3d40cb1ace..524a9d7bd9 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb index 37186903ff..6cb9d5239d 100644 --- a/activerecord/test/models/customer_carrier.rb +++ b/activerecord/test/models/customer_carrier.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CustomerCarrier < ActiveRecord::Base cattr_accessor :current_customer diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb index 1b3b54545f..d25ceeafb1 100644 --- a/activerecord/test/models/dashboard.rb +++ b/activerecord/test/models/dashboard.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard < ActiveRecord::Base self.primary_key = :dashboard_id end diff --git a/activerecord/test/models/default.rb b/activerecord/test/models/default.rb index 887e9cc999..90f1046d87 100644 --- a/activerecord/test/models/default.rb +++ b/activerecord/test/models/default.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Default < ActiveRecord::Base end diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb index 08004a0ed3..868b9bf4bf 100644 --- a/activerecord/test/models/department.rb +++ b/activerecord/test/models/department.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Department < ActiveRecord::Base has_many :chefs belongs_to :hotel diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 654830ba11..8881c69368 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "ostruct" module DeveloperProjectsAssociationExtension2 @@ -85,6 +87,17 @@ class Developer < ActiveRecord::Base private :track_instance_count end +class SubDeveloper < Developer +end + +class SymbolIgnoredDeveloper < ActiveRecord::Base + self.table_name = "developers" + self.ignored_columns = [:first_name, :last_name] + + attr_accessor :last_name + define_attribute_method "last_name" +end + class AuditLog < ActiveRecord::Base belongs_to :developer, validate: true belongs_to :unvalidated_developer, class_name: "Developer" diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb index b02b8447b8..75d284ac25 100644 --- a/activerecord/test/models/dog.rb +++ b/activerecord/test/models/dog.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dog < ActiveRecord::Base belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb index 2c5be94aea..aabe914f77 100644 --- a/activerecord/test/models/dog_lover.rb +++ b/activerecord/test/models/dog_lover.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DogLover < ActiveRecord::Base has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id diff --git a/activerecord/test/models/doubloon.rb b/activerecord/test/models/doubloon.rb index 7272504666..febadc3a5a 100644 --- a/activerecord/test/models/doubloon.rb +++ b/activerecord/test/models/doubloon.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbstractDoubloon < ActiveRecord::Base # This has functionality that might be shared by multiple classes. diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb index 2db968ef11..eb6701b84e 100644 --- a/activerecord/test/models/drink_designer.rb +++ b/activerecord/test/models/drink_designer.rb @@ -1,3 +1,8 @@ +# frozen_string_literal: true + class DrinkDesigner < ActiveRecord::Base has_one :chef, as: :employable end + +class MocktailDesigner < DrinkDesigner +end diff --git a/activerecord/test/models/edge.rb b/activerecord/test/models/edge.rb index e61d25c9bc..a04ab103de 100644 --- a/activerecord/test/models/edge.rb +++ b/activerecord/test/models/edge.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class models an edge in a directed graph. class Edge < ActiveRecord::Base belongs_to :source, class_name: "Vertex", foreign_key: "source_id" diff --git a/activerecord/test/models/electron.rb b/activerecord/test/models/electron.rb index 6fc270673f..902006b314 100644 --- a/activerecord/test/models/electron.rb +++ b/activerecord/test/models/electron.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Electron < ActiveRecord::Base belongs_to :molecule diff --git a/activerecord/test/models/engine.rb b/activerecord/test/models/engine.rb index eada171f6a..396a52b3b9 100644 --- a/activerecord/test/models/engine.rb +++ b/activerecord/test/models/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Engine < ActiveRecord::Base belongs_to :my_car, class_name: "Car", foreign_key: "car_id", counter_cache: :engines_count end diff --git a/activerecord/test/models/entrant.rb b/activerecord/test/models/entrant.rb index 4682ce48c8..2c086e451f 100644 --- a/activerecord/test/models/entrant.rb +++ b/activerecord/test/models/entrant.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Entrant < ActiveRecord::Base belongs_to :course end diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 1f9772870e..e59db4d877 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Essay < ActiveRecord::Base belongs_to :author belongs_to :writer, primary_key: :name, polymorphic: true diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb index 365ab32b0b..a7cdc39e5c 100644 --- a/activerecord/test/models/event.rb +++ b/activerecord/test/models/event.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Event < ActiveRecord::Base validates_uniqueness_of :title end diff --git a/activerecord/test/models/eye.rb b/activerecord/test/models/eye.rb index f53c34e4b1..f3608b62ef 100644 --- a/activerecord/test/models/eye.rb +++ b/activerecord/test/models/eye.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Eye < ActiveRecord::Base attr_reader :after_create_callbacks_stack attr_reader :after_update_callbacks_stack diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 5913bfa969..948435136d 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Face < ActiveRecord::Base belongs_to :man, inverse_of: :face belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face @@ -6,4 +8,8 @@ class Face < ActiveRecord::Base # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face + + validate do + man + end end diff --git a/activerecord/test/models/family.rb b/activerecord/test/models/family.rb index 5ae5a78c95..0713dba1a6 100644 --- a/activerecord/test/models/family.rb +++ b/activerecord/test/models/family.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Family < ActiveRecord::Base has_many :family_trees, -> { where(token: nil) } has_many :members, through: :family_trees diff --git a/activerecord/test/models/family_tree.rb b/activerecord/test/models/family_tree.rb index cd9829fedd..a8ea907c05 100644 --- a/activerecord/test/models/family_tree.rb +++ b/activerecord/test/models/family_tree.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FamilyTree < ActiveRecord::Base belongs_to :member, class_name: "User", foreign_key: "member_id" belongs_to :family diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb index 578382b494..9f1712a8ec 100644 --- a/activerecord/test/models/friendship.rb +++ b/activerecord/test/models/friendship.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Friendship < ActiveRecord::Base belongs_to :friend, class_name: "Person" # friend_too exists to test a bug, and probably shouldn't be used elsewhere diff --git a/activerecord/test/models/guid.rb b/activerecord/test/models/guid.rb index 05653ba498..ec71c37690 100644 --- a/activerecord/test/models/guid.rb +++ b/activerecord/test/models/guid.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Guid < ActiveRecord::Base end diff --git a/activerecord/test/models/guitar.rb b/activerecord/test/models/guitar.rb index cd068ff53d..649b998665 100644 --- a/activerecord/test/models/guitar.rb +++ b/activerecord/test/models/guitar.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Guitar < ActiveRecord::Base has_many :tuning_pegs, index_errors: true accepts_nested_attributes_for :tuning_pegs diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb index 7bc717c891..1a433c3cab 100644 --- a/activerecord/test/models/hotel.rb +++ b/activerecord/test/models/hotel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Hotel < ActiveRecord::Base has_many :departments has_many :chefs, through: :departments diff --git a/activerecord/test/models/image.rb b/activerecord/test/models/image.rb index 7ae8e4a7f6..b4808293cc 100644 --- a/activerecord/test/models/image.rb +++ b/activerecord/test/models/image.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Image < ActiveRecord::Base belongs_to :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class end diff --git a/activerecord/test/models/interest.rb b/activerecord/test/models/interest.rb index ec79416ee7..899b8f9b9d 100644 --- a/activerecord/test/models/interest.rb +++ b/activerecord/test/models/interest.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Interest < ActiveRecord::Base belongs_to :man, inverse_of: :interests belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_interests diff --git a/activerecord/test/models/invoice.rb b/activerecord/test/models/invoice.rb index 4be5a00193..1851792ed5 100644 --- a/activerecord/test/models/invoice.rb +++ b/activerecord/test/models/invoice.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Invoice < ActiveRecord::Base has_many :line_items, autosave: true before_save { |record| record.balance = record.line_items.map(&:amount).sum } diff --git a/activerecord/test/models/item.rb b/activerecord/test/models/item.rb index 336fb1769a..8d079d56e6 100644 --- a/activerecord/test/models/item.rb +++ b/activerecord/test/models/item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbstractItem < ActiveRecord::Base self.abstract_class = true has_one :tagging, as: :taggable diff --git a/activerecord/test/models/job.rb b/activerecord/test/models/job.rb index bbaef2792c..52817a8435 100644 --- a/activerecord/test/models/job.rb +++ b/activerecord/test/models/job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Job < ActiveRecord::Base has_many :references has_many :people, through: :references diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb index eeb5818a1f..436ffb6471 100644 --- a/activerecord/test/models/joke.rb +++ b/activerecord/test/models/joke.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Joke < ActiveRecord::Base self.table_name = "funny_jokes" end diff --git a/activerecord/test/models/keyboard.rb b/activerecord/test/models/keyboard.rb index bcede53ec9..d200e0fb56 100644 --- a/activerecord/test/models/keyboard.rb +++ b/activerecord/test/models/keyboard.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Keyboard < ActiveRecord::Base self.primary_key = "key_number" end diff --git a/activerecord/test/models/legacy_thing.rb b/activerecord/test/models/legacy_thing.rb index eead181a0e..e0210c8922 100644 --- a/activerecord/test/models/legacy_thing.rb +++ b/activerecord/test/models/legacy_thing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LegacyThing < ActiveRecord::Base self.locking_column = :version end diff --git a/activerecord/test/models/lesson.rb b/activerecord/test/models/lesson.rb index 4c88153068..e546339689 100644 --- a/activerecord/test/models/lesson.rb +++ b/activerecord/test/models/lesson.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LessonError < Exception end diff --git a/activerecord/test/models/line_item.rb b/activerecord/test/models/line_item.rb index 93f7cceb13..3a51cf03b2 100644 --- a/activerecord/test/models/line_item.rb +++ b/activerecord/test/models/line_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LineItem < ActiveRecord::Base belongs_to :invoice, touch: true end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 69d4d7df1a..b2fd305d66 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Liquid < ActiveRecord::Base self.table_name = :liquid has_many :molecules, -> { distinct } diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index d2436a735c..3acd89a48e 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Man < ActiveRecord::Base has_one :face, inverse_of: :man has_one :polymorphic_face, class_name: "Face", as: :polymorphic_man, inverse_of: :polymorphic_man diff --git a/activerecord/test/models/matey.rb b/activerecord/test/models/matey.rb index 80ee5f47c5..a77ac21e96 100644 --- a/activerecord/test/models/matey.rb +++ b/activerecord/test/models/matey.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Matey < ActiveRecord::Base belongs_to :pirate belongs_to :target, class_name: "Pirate" diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index b9597c6b9a..4315ba1941 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Member < ActiveRecord::Base has_one :current_membership has_one :selected_membership diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 157130986c..87f7aab9a2 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MemberDetail < ActiveRecord::Base belongs_to :member, inverse_of: false belongs_to :organization diff --git a/activerecord/test/models/member_type.rb b/activerecord/test/models/member_type.rb index a13561c72a..b49b168d03 100644 --- a/activerecord/test/models/member_type.rb +++ b/activerecord/test/models/member_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MemberType < ActiveRecord::Base has_many :members end diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index 0f8be0ad85..09ee7544b3 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Membership < ActiveRecord::Base enum type: %i(Membership CurrentMembership SuperMembership SelectedMembership TenantMembership) belongs_to :member diff --git a/activerecord/test/models/mentor.rb b/activerecord/test/models/mentor.rb index 66504b4e91..2fbb62c435 100644 --- a/activerecord/test/models/mentor.rb +++ b/activerecord/test/models/mentor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Mentor < ActiveRecord::Base has_many :developers end diff --git a/activerecord/test/models/minimalistic.rb b/activerecord/test/models/minimalistic.rb index 2e3f8e081a..c67b086853 100644 --- a/activerecord/test/models/minimalistic.rb +++ b/activerecord/test/models/minimalistic.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Minimalistic < ActiveRecord::Base end diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb index e9b05dadf2..d9d331798a 100644 --- a/activerecord/test/models/minivan.rb +++ b/activerecord/test/models/minivan.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Minivan < ActiveRecord::Base self.primary_key = :minivan_id diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 1c35006665..8e92f68817 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MixedCaseMonkey < ActiveRecord::Base belongs_to :man end diff --git a/activerecord/test/models/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb deleted file mode 100644 index 77b44651a3..0000000000 --- a/activerecord/test/models/mocktail_designer.rb +++ /dev/null @@ -1,2 +0,0 @@ -class MocktailDesigner < DrinkDesigner -end diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb index 26870c8f88..7da08a85c4 100644 --- a/activerecord/test/models/molecule.rb +++ b/activerecord/test/models/molecule.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Molecule < ActiveRecord::Base belongs_to :liquid has_many :electrons diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb index 0302abad1e..fa2ea900c7 100644 --- a/activerecord/test/models/movie.rb +++ b/activerecord/test/models/movie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Movie < ActiveRecord::Base self.primary_key = "movieid" diff --git a/activerecord/test/models/node.rb b/activerecord/test/models/node.rb index 459ea8cf95..ae46c76b46 100644 --- a/activerecord/test/models/node.rb +++ b/activerecord/test/models/node.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Node < ActiveRecord::Base belongs_to :tree, touch: true belongs_to :parent, class_name: "Node", touch: true, optional: true diff --git a/activerecord/test/models/non_primary_key.rb b/activerecord/test/models/non_primary_key.rb index 1cafb09608..e954375989 100644 --- a/activerecord/test/models/non_primary_key.rb +++ b/activerecord/test/models/non_primary_key.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class NonPrimaryKey < ActiveRecord::Base end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb index 82edc64b68..3f8728af5e 100644 --- a/activerecord/test/models/notification.rb +++ b/activerecord/test/models/notification.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Notification < ActiveRecord::Base validates_presence_of :message end diff --git a/activerecord/test/models/numeric_data.rb b/activerecord/test/models/numeric_data.rb index c6e025a9ce..666e1a5778 100644 --- a/activerecord/test/models/numeric_data.rb +++ b/activerecord/test/models/numeric_data.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NumericData < ActiveRecord::Base self.table_name = "numeric_data" # Decimal columns with 0 scale being automatically treated as integers diff --git a/activerecord/test/models/order.rb b/activerecord/test/models/order.rb index 699be53959..36866b398f 100644 --- a/activerecord/test/models/order.rb +++ b/activerecord/test/models/order.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Order < ActiveRecord::Base belongs_to :billing, class_name: "Customer", foreign_key: "billing_customer_id" belongs_to :shipping, class_name: "Customer", foreign_key: "shipping_customer_id" diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 462830dadc..099e7e38e0 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Organization < ActiveRecord::Base has_many :member_details has_many :members, through: :member_details diff --git a/activerecord/test/models/other_dog.rb b/activerecord/test/models/other_dog.rb index 418caf34be..a0fda5ae1b 100644 --- a/activerecord/test/models/other_dog.rb +++ b/activerecord/test/models/other_dog.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_dependency "models/arunit2_model" class OtherDog < ARUnit2Model diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 21fc9b6eb8..5fa50d9918 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Owner < ActiveRecord::Base self.primary_key = :owner_id has_many :pets, -> { order "pets.name desc" } diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index 1e5f9285a8..ba9ddb8c6a 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Parrot < ActiveRecord::Base self.inheritance_column = :parrot_sti_class @@ -8,7 +10,7 @@ class Parrot < ActiveRecord::Base validates_presence_of :name - attr_accessor :cancel_save_from_callback + attribute :cancel_save_from_callback before_save :cancel_save_callback_method, if: :cancel_save_from_callback def cancel_save_callback_method throw(:abort) diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 18994d6f18..5cba1e440e 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Person < ActiveRecord::Base has_many :readers has_many :secure_readers diff --git a/activerecord/test/models/personal_legacy_thing.rb b/activerecord/test/models/personal_legacy_thing.rb index adde7a504a..ed8b70cfcc 100644 --- a/activerecord/test/models/personal_legacy_thing.rb +++ b/activerecord/test/models/personal_legacy_thing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonalLegacyThing < ActiveRecord::Base self.locking_column = :version belongs_to :person, counter_cache: true diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 51a3e42815..9bda2109e6 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Pet < ActiveRecord::Base attr_accessor :current_user diff --git a/activerecord/test/models/pet_treasure.rb b/activerecord/test/models/pet_treasure.rb index 1fe7807ffe..47b9f57fad 100644 --- a/activerecord/test/models/pet_treasure.rb +++ b/activerecord/test/models/pet_treasure.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PetTreasure < ActiveRecord::Base self.table_name = "pets_treasures" diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index c532ab426e..c8617d1cfe 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Pirate < ActiveRecord::Base belongs_to :parrot, validate: true belongs_to :non_validated_parrot, class_name: "Parrot" diff --git a/activerecord/test/models/possession.rb b/activerecord/test/models/possession.rb index 0226336c16..9b843e1525 100644 --- a/activerecord/test/models/possession.rb +++ b/activerecord/test/models/possession.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Possession < ActiveRecord::Base self.table_name = "having" end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index ed64e0ee52..780a2c17f5 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Post < ActiveRecord::Base class CategoryPost < ActiveRecord::Base self.table_name = "categories_posts" @@ -19,7 +21,7 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") } - scope :ranked_by_comments, -> { order("comments_count DESC") } + scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) } scope :limit_by, lambda { |l| limit(l) } scope :locked, -> { lock } @@ -113,6 +115,7 @@ class Post < ActiveRecord::Base has_many :misc_tags, -> { where tags: { name: "Misc" } }, through: :taggings, source: :tag has_many :funky_tags, through: :taggings, source: :tag has_many :super_tags, through: :taggings + has_many :ordered_tags, through: :taggings has_many :tags_with_primary_key, through: :taggings, source: :tag_with_primary_key has_one :tagging, as: :taggable @@ -182,14 +185,19 @@ end class SpecialPost < Post; end class StiPost < Post - self.abstract_class = true has_one :special_comment, class_name: "SpecialComment" end +class AbstractStiPost < Post + self.abstract_class = true +end + class SubStiPost < StiPost self.table_name = Post.table_name end +class SubAbstractStiPost < AbstractStiPost; end + class FirstPost < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" @@ -199,6 +207,11 @@ class FirstPost < ActiveRecord::Base has_one :comment, foreign_key: :post_id end +class TaggedPost < Post + has_many :taggings, -> { rewhere(taggable_type: "TaggedPost") }, as: :taggable + has_many :tags, through: :taggings +end + class PostWithDefaultInclude < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" @@ -276,3 +289,39 @@ end class SubConditionalStiPost < ConditionalStiPost end + +class FakeKlass + extend ActiveRecord::Delegation::DelegateCache + + inherited self + + class << self + def connection + Post.connection + end + + def table_name + "posts" + end + + def attribute_alias?(name) + false + end + + def sanitize_sql(sql) + sql + end + + def sanitize_sql_for_order(sql) + sql + end + + def arel_attribute(name, table) + table[name] + end + + def enforce_raw_sql_whitelist(*args) + # noop + end + end +end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index ce086e40a3..f1f88d8d8d 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PriceEstimate < ActiveRecord::Base belongs_to :estimate_of, polymorphic: true belongs_to :thing, polymorphic: true diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb index 4dfb1a9602..abc23f40ff 100644 --- a/activerecord/test/models/professor.rb +++ b/activerecord/test/models/professor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_dependency "models/arunit2_model" class Professor < ARUnit2Model diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 4fbd986e40..846cef625b 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Project < ActiveRecord::Base belongs_to :mentor has_and_belongs_to_many :developers, -> { distinct.order "developers.name desc, developers.id desc" } diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb index 0d4a7f9235..53677197c4 100644 --- a/activerecord/test/models/publisher.rb +++ b/activerecord/test/models/publisher.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module Publisher end diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb index d73a8eb936..355c22dcc5 100644 --- a/activerecord/test/models/publisher/article.rb +++ b/activerecord/test/models/publisher/article.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Publisher::Article < ActiveRecord::Base has_and_belongs_to_many :magazines has_and_belongs_to_many :tags diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb index 82e1a14008..425ede8df2 100644 --- a/activerecord/test/models/publisher/magazine.rb +++ b/activerecord/test/models/publisher/magazine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Publisher::Magazine < ActiveRecord::Base has_and_belongs_to_many :articles end diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb index d4be1e13b4..f90a7b9336 100644 --- a/activerecord/test/models/randomly_named_c1.rb +++ b/activerecord/test/models/randomly_named_c1.rb @@ -1,3 +1,5 @@ -class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table1
-end
+# frozen_string_literal: true + +class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base + self.table_name = :randomly_named_table1 +end diff --git a/activerecord/test/models/rating.rb b/activerecord/test/models/rating.rb index 7420821db0..cf06bc6931 100644 --- a/activerecord/test/models/rating.rb +++ b/activerecord/test/models/rating.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rating < ActiveRecord::Base belongs_to :comment has_many :taggings, as: :taggable diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index 7c5a159fe0..d25627e430 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Reader < ActiveRecord::Base belongs_to :post belongs_to :person, inverse_of: :readers diff --git a/activerecord/test/models/recipe.rb b/activerecord/test/models/recipe.rb index c387230603..e53f5c8fb1 100644 --- a/activerecord/test/models/recipe.rb +++ b/activerecord/test/models/recipe.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Recipe < ActiveRecord::Base belongs_to :chef end diff --git a/activerecord/test/models/record.rb b/activerecord/test/models/record.rb index f77ac9fc03..63143e296a 100644 --- a/activerecord/test/models/record.rb +++ b/activerecord/test/models/record.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Record < ActiveRecord::Base end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index e2bb980fed..2a7a1e3b77 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Reference < ActiveRecord::Base belongs_to :person belongs_to :job diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index a2d169292a..bc829ec67f 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "models/topic" class Reply < Topic diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 77a7b22315..7973219a79 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ship < ActiveRecord::Base self.record_timestamps = false diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb index 1a633b8d77..f6d7a8ae5e 100644 --- a/activerecord/test/models/ship_part.rb +++ b/activerecord/test/models/ship_part.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShipPart < ActiveRecord::Base belongs_to :ship has_many :trinkets, class_name: "Treasure", as: :looter diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb index f9d23d13b0..92afe70b92 100644 --- a/activerecord/test/models/shop.rb +++ b/activerecord/test/models/shop.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Shop class Collection < ActiveRecord::Base has_many :products, dependent: :nullify diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb index 1580e8b20c..97fb058331 100644 --- a/activerecord/test/models/shop_account.rb +++ b/activerecord/test/models/shop_account.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShopAccount < ActiveRecord::Base belongs_to :customer belongs_to :customer_carrier diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb index 497c3aba9a..e456907a22 100644 --- a/activerecord/test/models/speedometer.rb +++ b/activerecord/test/models/speedometer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Speedometer < ActiveRecord::Base self.primary_key = :speedometer_id belongs_to :dashboard diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index 3f142b25fe..f190860fd1 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id" belongs_to :sponsorable, polymorphic: true diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb index f084ec1bdc..473c145f4c 100644 --- a/activerecord/test/models/string_key_object.rb +++ b/activerecord/test/models/string_key_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class StringKeyObject < ActiveRecord::Base self.primary_key = :id end diff --git a/activerecord/test/models/student.rb b/activerecord/test/models/student.rb index 28a0b6c99b..e750798f74 100644 --- a/activerecord/test/models/student.rb +++ b/activerecord/test/models/student.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Student < ActiveRecord::Base has_and_belongs_to_many :lessons belongs_to :college diff --git a/activerecord/test/models/subscriber.rb b/activerecord/test/models/subscriber.rb index a820329003..b21969ca2d 100644 --- a/activerecord/test/models/subscriber.rb +++ b/activerecord/test/models/subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Subscriber < ActiveRecord::Base self.primary_key = "nick" has_many :subscriptions diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb index 1cedf6deae..d1d5d21621 100644 --- a/activerecord/test/models/subscription.rb +++ b/activerecord/test/models/subscription.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Subscription < ActiveRecord::Base belongs_to :subscriber, counter_cache: :books_count belongs_to :book diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index c907aea10f..c1a8890a8a 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tag < ActiveRecord::Base has_many :taggings has_many :taggables, through: :taggings @@ -9,5 +11,6 @@ end class OrderedTag < Tag self.table_name = "tags" - has_many :taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id" + has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging" + has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post" end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f739b4a197..861fde633f 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # test that attr_readonly isn't called on the :taggable polymorphic association module Taggable end @@ -6,6 +8,7 @@ class Tagging < ActiveRecord::Base belongs_to :tag, -> { includes(:tagging) } belongs_to :super_tag, class_name: "Tag", foreign_key: "super_tag_id" belongs_to :invalid_tag, class_name: "Tag", foreign_key: "tag_id" + belongs_to :ordered_tag, class_name: "OrderedTag", foreign_key: "tag_id" belongs_to :blue_tag, -> { where tags: { name: "Blue" } }, class_name: "Tag", foreign_key: :tag_id belongs_to :tag_with_primary_key, class_name: "Tag", foreign_key: :tag_id, primary_key: :custom_primary_key belongs_to :taggable, polymorphic: true, counter_cache: :tags_count diff --git a/activerecord/test/models/task.rb b/activerecord/test/models/task.rb index e36989dd56..dabe3ce06b 100644 --- a/activerecord/test/models/task.rb +++ b/activerecord/test/models/task.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Task < ActiveRecord::Base def updated_at ending diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index d9381ac9cf..8cd4dc352a 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Topic < ActiveRecord::Base scope :base, -> { all } scope :written_before, lambda { |time| @@ -63,6 +65,9 @@ class Topic < ActiveRecord::Base after_initialize :set_email_address + attr_accessor :change_approved_before_save + before_save :change_approved_callback + class_attribute :after_initialize_called after_initialize do self.class.after_initialize_called = true @@ -94,6 +99,10 @@ class Topic < ActiveRecord::Base def before_destroy_for_transaction; end def after_save_for_transaction; end def after_create_for_transaction; end + + def change_approved_callback + self.approved = change_approved_before_save unless change_approved_before_save.nil? + end end class ImportantTopic < Topic diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index ddc7048a56..4a5697eeb1 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Toy < ActiveRecord::Base self.primary_key = :toy_id belongs_to :pet diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb index a6b7edb882..0b88815cbd 100644 --- a/activerecord/test/models/traffic_light.rb +++ b/activerecord/test/models/traffic_light.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TrafficLight < ActiveRecord::Base serialize :state, Array serialize :long_state, Array diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index fb2a5d44e2..b51db56c37 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Treasure < ActiveRecord::Base has_and_belongs_to_many :parrots belongs_to :looter, polymorphic: true diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb index 373cd48f71..5c1d75aa09 100644 --- a/activerecord/test/models/treaty.rb +++ b/activerecord/test/models/treaty.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Treaty < ActiveRecord::Base self.primary_key = :treaty_id diff --git a/activerecord/test/models/tree.rb b/activerecord/test/models/tree.rb index dc29cccc9c..77050c5fff 100644 --- a/activerecord/test/models/tree.rb +++ b/activerecord/test/models/tree.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tree < ActiveRecord::Base has_many :nodes, dependent: :destroy end diff --git a/activerecord/test/models/tuning_peg.rb b/activerecord/test/models/tuning_peg.rb index 1252d6dc1d..6e052e1d0f 100644 --- a/activerecord/test/models/tuning_peg.rb +++ b/activerecord/test/models/tuning_peg.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TuningPeg < ActiveRecord::Base belongs_to :guitar validates_numericality_of :pitch diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb index e50a21ca68..d627026585 100644 --- a/activerecord/test/models/tyre.rb +++ b/activerecord/test/models/tyre.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tyre < ActiveRecord::Base belongs_to :car diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index 5089a795f4..3efbc45d2a 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "models/job" class User < ActiveRecord::Base diff --git a/activerecord/test/models/uuid_child.rb b/activerecord/test/models/uuid_child.rb index a3d0962ad6..9fce361cc8 100644 --- a/activerecord/test/models/uuid_child.rb +++ b/activerecord/test/models/uuid_child.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UuidChild < ActiveRecord::Base belongs_to :uuid_parent end diff --git a/activerecord/test/models/uuid_item.rb b/activerecord/test/models/uuid_item.rb index 2353e40213..41f68c4c18 100644 --- a/activerecord/test/models/uuid_item.rb +++ b/activerecord/test/models/uuid_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UuidItem < ActiveRecord::Base end diff --git a/activerecord/test/models/uuid_parent.rb b/activerecord/test/models/uuid_parent.rb index 5634f22d0c..05db61855e 100644 --- a/activerecord/test/models/uuid_parent.rb +++ b/activerecord/test/models/uuid_parent.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UuidParent < ActiveRecord::Base has_many :uuid_children end diff --git a/activerecord/test/models/vegetables.rb b/activerecord/test/models/vegetables.rb index a4590d06e0..cfaab08ed1 100644 --- a/activerecord/test/models/vegetables.rb +++ b/activerecord/test/models/vegetables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Vegetable < ActiveRecord::Base validates_presence_of :name diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb index 855bc4e325..c9b3338522 100644 --- a/activerecord/test/models/vehicle.rb +++ b/activerecord/test/models/vehicle.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Vehicle < ActiveRecord::Base self.abstract_class = true default_scope -> { where("tires_count IS NOT NULL") } diff --git a/activerecord/test/models/vertex.rb b/activerecord/test/models/vertex.rb index 3d19433b6f..0ad8114898 100644 --- a/activerecord/test/models/vertex.rb +++ b/activerecord/test/models/vertex.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class models a vertex in a directed graph. class Vertex < ActiveRecord::Base has_many :sink_edges, class_name: "Edge", foreign_key: "source_id" diff --git a/activerecord/test/models/warehouse_thing.rb b/activerecord/test/models/warehouse_thing.rb index f20bd1a245..099633af55 100644 --- a/activerecord/test/models/warehouse_thing.rb +++ b/activerecord/test/models/warehouse_thing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WarehouseThing < ActiveRecord::Base self.table_name = "warehouse-things" diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb index cba2b3e518..8db57d181e 100644 --- a/activerecord/test/models/wheel.rb +++ b/activerecord/test/models/wheel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Wheel < ActiveRecord::Base - belongs_to :wheelable, polymorphic: true, counter_cache: true + belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: true end diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 7c0fc286e1..fc0a52d6ad 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WithoutTable < ActiveRecord::Base default_scope -> { where(published: true) } end diff --git a/activerecord/test/models/zine.rb b/activerecord/test/models/zine.rb index 3f2b348b46..6f361665ef 100644 --- a/activerecord/test/models/zine.rb +++ b/activerecord/test/models/zine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Zine < ActiveRecord::Base has_many :interests, inverse_of: :zine end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 90a314c83c..e634e9e6b1 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do if ActiveRecord::Base.connection.version >= "5.6.0" diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index 264d9b8910..e236571caa 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do execute "drop table test_oracle_defaults" rescue nil diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index e56e8fa36a..f15178d695 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do enable_extension!("uuid-ossp", ActiveRecord::Base.connection) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index f534e9c00e..3205c4c20a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do # ------------------------------------------------------------------- # # # @@ -7,7 +9,7 @@ ActiveRecord::Schema.define do # ------------------------------------------------------------------- # create_table :accounts, force: true do |t| - t.integer :firm_id + t.references :firm, index: false t.string :firm_name t.integer :credit_limit end @@ -121,7 +123,7 @@ ActiveRecord::Schema.define do create_table :cars, force: true do |t| t.string :name t.integer :engines_count - t.integer :wheels_count + t.integer :wheels_count, default: 0 t.column :lock_version, :integer, null: false, default: 0 t.timestamps null: false end @@ -189,17 +191,22 @@ ActiveRecord::Schema.define do t.string :resource_id t.string :resource_type t.integer :developer_id + t.datetime :updated_at + t.datetime :deleted_at + t.integer :comments end create_table :companies, force: true do |t| t.string :type - t.integer :firm_id + t.references :firm, index: false t.string :firm_name t.string :name - t.integer :client_of - t.integer :rating, default: 1 + t.bigint :client_of + t.bigint :rating, default: 1 t.integer :account_id t.string :description, default: "" + t.index [:name, :rating], order: :desc + t.index [:name, :description], length: 10 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 @@ -232,8 +239,8 @@ ActiveRecord::Schema.define do end create_table :contracts, force: true do |t| - t.integer :developer_id - t.integer :company_id + t.references :developer, index: false + t.references :company, index: false end create_table :customers, force: true do |t| @@ -259,7 +266,7 @@ ActiveRecord::Schema.define do t.string :name t.string :first_name t.integer :salary, default: 70000 - t.integer :firm_id + t.references :firm, index: false t.integer :mentor_id if subsecond_precision_supported? t.datetime :created_at, precision: 6 @@ -716,7 +723,7 @@ ActiveRecord::Schema.define do create_table :projects, force: true do |t| t.string :name t.string :type - t.integer :firm_id + t.references :firm, index: false t.integer :mentor_id end @@ -805,8 +812,7 @@ ActiveRecord::Schema.define do create_table :sponsors, force: true do |t| t.integer :club_id - t.integer :sponsorable_id - t.string :sponsorable_type + t.references :sponsorable, polymorphic: true, index: false end create_table :string_key_objects, id: false, force: true do |t| @@ -958,6 +964,7 @@ ActiveRecord::Schema.define do end create_table :wheels, force: true do |t| + t.integer :size t.references :wheelable, polymorphic: true end diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb index aaff408b41..bd6d5c339b 100644 --- a/activerecord/test/support/config.rb +++ b/activerecord/test/support/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "yaml" require "erb" require "fileutils" diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 1a609e13c3..2a4fa53460 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/logger" require "models/college" require "models/course" diff --git a/activerecord/test/support/connection_helper.rb b/activerecord/test/support/connection_helper.rb index 4a19e5df44..3bb1b370c1 100644 --- a/activerecord/test/support/connection_helper.rb +++ b/activerecord/test/support/connection_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ConnectionHelper def run_without_connection original_connection = ActiveRecord::Base.remove_connection diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 43cb235e01..a18bf5ea0a 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DdlHelper def with_example_table(connection, table_name, definition = nil) connection.execute("CREATE TABLE #{table_name}(#{definition})") diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 666c1b6a14..777e6a7c1b 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SchemaDumpingHelper def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables |
